【Rails】includesメソッドの挙動をざっくりまとめてみた
1.この記事をなぜ書いたか
includesメソッドの挙動は複雑です。複雑すぎて自分はよく忘れます笑。
そのため、情報をまとめたいと思い記事を作成しました。
メソッドチェイン時のincludesメソッドの使い方を知りたい方は、4章から読むことをオススメします。
記事の中で間違いがある場合、コメント頂けると嬉しいです。
2.includesメソッドとはそもそも何か
includesメソッドは、親子関係のデータリソースをまとめてDBから取得できるメソッドです。
また、N+1問題を解決する際によく使います。
3.なぜincludesメソッドを使うのか
先程も書きましたが、includesメソッドを使う理由は、N+1問題を解決するためです。 まず、includesメソッドを使わない例から確認して行きます。
- includesメソッドを用いない例
以下のコードではincludesメソッドを使わずwhereメソッドを用いて、クエリをDBへ発行しています。そして、条件を満たすリソースを取得して@commentsに代入しています。その後、show.html.erbを呼び出します。そして、show.html.erb内で@comments内のコメントの個数分、_commnt.html.erbを繰り返し呼び出します。
def show
@board = Board.find(params[:id])
@comments = Comment.where(board_id: params[:id]).order("created_at desc")
end
<!-- コメントエリア -->
<div id="js-comment-area">
<%= render @comments %>
</div>
<h3 class="small"><%= comment.user.decorate.full_name %></h3>
以下の画像は、上のコードを実行した際のブラウザ上の画面です。画像内にコメントが2個あります。そして、それらのコメント内には作成者の名前が表示されています。以下のような手順で、コメントの作成者名を取得しています。
- show.html.erbから_comment.html.erbを呼び出す。
- _comment.html.erb内のcomment.userを実行した際にクエリがDBへ発行される。
- コメントの作成者を取得する(userはアソシエーションメソッドです)。
- 取得したコメントの作成者に対して、full_nameメソッドを使う。
- 作成者名を画面に表示できる。
- コメントの数だけ1~5の手順をループする。
しかし、これには問題があります。理由は、コメントを表示させるのに、コメントの数だけクエリをDBへ発行する必要があるからです。以下の画像の場合、1つの掲示板のコメントを全て取得するのにクエリを1回発行します。その後、コメント作成者を取得するのにクエリを2回発行しています。コメントが2個しかないので、クエリの発行が2回で済んでますが、コメントが大量にあると、その数だけクエリを発行しなければいけません。その結果、処理に時間がかかり、Webアプリケーションのパフォーマンスが落ちてしまいます。クエリの数に問題があるので、この問題のことを1+N問題またはN+1問題と呼びます。 N+1問題を解決する方法は色々あるそうですが、今回はincludesメソッド使って解決します。
サーバーのログを見ると、クエリが大量に発行されていることが分かります。以下の画像内の最後のSELECT文と最後から2番目のSELECT文で、各コメントの作成者を取得しています。
次にincludesメソッドを用いる例を確認します。
- includesメソッドを用いる例
先程のコードから、showアクション内部のコードのみ変更しました。includesメソッドを使うことによって、whereメソッドで取得したコメントの外部キーの値と主キーの値が一致するユーザー(コメントの作成者)を全て取得できます。そのため、_comment.html.erbでcomment.userを実行した際に、クエリがDBへ発行されることはありません。理由は、コメントの作成者をincludesメソッドで既に取得しているからです。
def show
@board = Board.find(params[:id])
@comments = Comment.where(board_id: params[:id]).includes(:user).order("created_at desc")
end
<!-- コメントエリア -->
<div id="js-comment-area">
<%= render @comments %>
</div>
<h3 class="small"><%= comment.user.decorate.full_name %></h3>
サーバーのログを見ると、クエリ数が減少していることが分かります。以下の画像内の最後のSELECT文で、各コメントの作成者を取得しています。
以上を踏まえて、includesメソッドを使う理由が、N+1問題を解決するためであることが分かりました。
4.includesメソッドの使い方の種類
個人的に重要だと感じた使い方についてまとめました。
- 一般的なクエリメソッドとしてincludesメソッドを使う
- メソッドチェインとしてincludesメソッドを使う
順番に説明して行きます。
- 一般的なクエリメソッドとしてincludesメソッドを使う
基本的には以下のコードのようにincludesメソッドを使います。includesメソッドを使う前に、親モデルと子モデルにアソシエーションを設定する必要があります。複数形の子モデル名を引数としてincludesメソッドを使うと、親モデルの全てのリソースの取得と、取得したリソースの主キーの値と一致している外部キーの値を持つ子モデルのリソースを全て取得します。単数形の親モデル名を引数としてincludesメソッドを使う場合は、子モデルの全てのリソースの取得と、取得したリソースの外部キーの値と一致している主キーの値を持つ親モデルのリソースを全て取得します。このようにincludesメソッドを使うと、クエリが2回発行されることが分かります。
親モデル名.includes(:複数形の子モデル名)
子モデル名.includes(:単数形の親モデル名)
- メソッドチェインとしてincludesメソッドを使う
3章で書いたコードのようにメソッドチェインとしてincludesメソッドを使うことができます。
メソッドチェインとは、ActiveRecord::Relationクラスのオブジェクトを返すメソッドに対して、メソッドを使うことができる仕組みです。以下のコードの場合、whereメソッドがActiveRecord::Relationクラスのオブジェクトを返すので、includesメソッドを使えます。以下のコードのようにincludesメソッドを使うと、whereメソッドによって条件を満たすリソースを取得した後に、取得したリソースの外部キーの値と一致する主キーを持つリソースを取得します。結果的に、whereメソッドのクエリとincludesメソッドのクエリを合計して、クエリが2回発行されることが分かりました。
@comments = Comment.where(board_id: params[:id]).includes(:user).order("created_at desc")
クエリメソッドとして使うincludesメソッドとメソッドチェイン時のincludesメソッドの違いは、メソッドチェイン時のincludesメソッドはクエリを1つしか発行していない点です。
(ターミナルの結果から判断しただけで、Githubのコードを読んだ訳ではないです。そのため、もしかしたら間違っているかもしれません。)
5.終わり
メソッドチェイン時のincludesメソッドの挙動を考えるのが難しかったです。
この記事が少しでも役に立てば幸いです!
6.参考文献
Discussion