🐇

【Rails】includesメソッドの挙動をざっくりまとめてみた

2021/07/18に公開

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を繰り返し呼び出します。
boards_controller.rb(showアクションのみ抜粋)
  def show
    @board = Board.find(params[:id])
    @comments = Comment.where(board_id: params[:id]).order("created_at desc")
  end
boards/show.html.erb(一部抜粋)
  <!-- コメントエリア -->
  <div id="js-comment-area">
    <%= render @comments %>
  </div>
comments/_comment.html.erb(一部抜粋)
  <h3 class="small"><%= comment.user.decorate.full_name %></h3>

以下の画像は、上のコードを実行した際のブラウザ上の画面です。画像内にコメントが2個あります。そして、それらのコメント内には作成者の名前が表示されています。以下のような手順で、コメントの作成者名を取得しています。

  1. show.html.erbから_comment.html.erbを呼び出す。
  2. _comment.html.erb内のcomment.userを実行した際にクエリがDBへ発行される。
  3. コメントの作成者を取得する(userはアソシエーションメソッドです)。
  4. 取得したコメントの作成者に対して、full_nameメソッドを使う。
  5. 作成者名を画面に表示できる。
  6. コメントの数だけ1~5の手順をループする。

しかし、これには問題があります。理由は、コメントを表示させるのに、コメントの数だけクエリをDBへ発行する必要があるからです。以下の画像の場合、1つの掲示板のコメントを全て取得するのにクエリを1回発行します。その後、コメント作成者を取得するのにクエリを2回発行しています。コメントが2個しかないので、クエリの発行が2回で済んでますが、コメントが大量にあると、その数だけクエリを発行しなければいけません。その結果、処理に時間がかかり、Webアプリケーションのパフォーマンスが落ちてしまいます。クエリの数に問題があるので、この問題のことを1+N問題またはN+1問題と呼びます。 N+1問題を解決する方法は色々あるそうですが、今回はincludesメソッド使って解決します。
Image from Gyazo
サーバーのログを見ると、クエリが大量に発行されていることが分かります。以下の画像内の最後のSELECT文と最後から2番目のSELECT文で、各コメントの作成者を取得しています。
Image from Gyazo

次にincludesメソッドを用いる例を確認します。

  • includesメソッドを用いる例
    先程のコードから、showアクション内部のコードのみ変更しました。includesメソッドを使うことによって、whereメソッドで取得したコメントの外部キーの値と主キーの値が一致するユーザー(コメントの作成者)を全て取得できます。そのため、_comment.html.erbでcomment.userを実行した際に、クエリがDBへ発行されることはありません。理由は、コメントの作成者をincludesメソッドで既に取得しているからです。
boards_controller.rb(showアクションのみ抜粋)
  def show
    @board = Board.find(params[:id])
    @comments = Comment.where(board_id: params[:id]).includes(:user).order("created_at desc")
  end
boards/show.html.erb(一部抜粋)
  <!-- コメントエリア -->
  <div id="js-comment-area">
    <%= render @comments %>
  </div>
comments/_comment.html.erb(一部抜粋)
  <h3 class="small"><%= comment.user.decorate.full_name %></h3>

サーバーのログを見ると、クエリ数が減少していることが分かります。以下の画像内の最後のSELECT文で、各コメントの作成者を取得しています。
Image from Gyazo

以上を踏まえて、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.参考文献

https://edgeapi.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-includes
https://pikawaka.com/rails/includes
https://railsguides.jp/active_record_querying.html

Discussion

ログインするとコメントできます