Yanonoblog!

こつこつと

JOINとRailsのincludesメソッドの備忘録

はじめに

N+1を解消するincludesメソッドを使用したときの挙動について、動作について調べて確認したため雑に書き残しておきたいと思います。

本記事では、RailsのincludeメソッドとSQLのJOINに関して簡単に解説していきます。

includesメソッド

要約

includesメソッドは、Active Recordを使用してデータベースから関連付けのあるデータをまとめて取得するために使用されます。

また、発生するN+1問題を解決することができます。

解説

includesを解説するにあたり前提として、UserモデルとPostモデルを用意します。

app/models/user.rb

class User < ApplicationRecord
  has_many :posts
end

app/models/post.rb

class Post < ApplicationRecord
  belongs_to :user
end

Userモデルには以下のレコードを保存します。

user1 = User.create(name: "User 1")
user2 = User.create(name: "User 2")

Postモデルには以下のレコードを保存します。

post1 = Post.create(title: "Post 1", body: "Body 1", user_id: 1)
post2 = Post.create(title: "Post 2", body: "Body 2", user_id: 2)
post3 = Post.create(title: "Post 3", body: "Body 3", user_id: 1)

この状態で、Post.allを使用してPostモデルの全てのレコードを取得すると、以下のようになります。

user_idはPostモデルのデータのため取得できますが、Userモデルのレコードは取得しません。

[<Post id: 1, title: "Post 1", body: "Body 1", user_id: 1>,
<Post id: 2, title: "Post 2", body: "Body 2", user_id: 2>,
<Post id: 3, title: "Post 3", body: "Body 3", user_id: 1>]

Post.includes(:user)とすることでPostモデルの全てのレコードとUserモデルのレコードをまとめて取得することができます。

[<Post id: 1, title: "Post 1", body: "Body 1", user_id: 1, user: <User id: 1, name: "User 1">>,
<Post id: 2, title: "Post 2", body: "Body 2", user_id: 2, user: <User id: 2, name: "User 2">>,
<Post id: 3, title: "Post 3", body: "Body 3", user_id: 1, user: <User id: 1, name: "User 1">>]

includeは N+1問題を解決する

上での解説からわかる通り、Post.includes(:user)を使用することで、Postモデルのレコードに加えて、それぞれに紐づいているUserモデルのレコードが含まれます。

includesメソッドを使用して関連付けされたデータもまとめて取得することでeach文などの繰り返し処理を行う際に余分なクエリを発行して発生してしまうN+1問題を発生させずに済むというメリットがあります。

includesを使わずにN+1問題を発生させる

以下のコードでは、User.allメソッドで全てのユーザーを取得し、eachメソッドでそれぞれのユーザーに関連する投稿を取得しています。

以下の処理方法の場合、ユーザーの数だけSQLクエリが発行されてN+1問題が発生します。

users = User.all

users.each do |user|
  posts = user.posts
  略
end

上記のコードによって発行されるSQLクエリの例です。

SELECT * FROM users
SELECT * FROM posts WHERE user_id = 1
SELECT * FROM posts WHERE user_id = 1
SELECT * FROM posts WHERE user_id = 2
SELECT * FROM posts WHERE user_id = 1
SELECT * FROM posts WHERE user_id = 3

以下の流れになります。

  1. 最初にすべてのユーザーを取得し、1行目のクエリが発行されます。
  2. 各ユーザーに対してそのユーザーの投稿を取得するために、各ユーザーに対して別々のクエリが発行されます。

includesを使用した場合のコード例とSQL文の例

繰り返し処理の中でUserに紐づくPostのレコード情報が欲しい場合は最初にincludesでpostのレコードも参照するようにします。

users = User.includes(:posts)

users.each do |user|
  posts = user.posts
  略
end

上記のコードによって発行されるSQLクエリの例です。

SELECT * FROM users
SELECT * FROM posts WHERE user_id IN (1, 2, 3, 4, 5)
  1. 最初にすべてのユーザーを取得し、1行目のクエリが発行されます。
  2. IN句を使ってそれぞれのuser_idに該当する投稿情報を一度に取得しています。

これによりN+1問題が発生せずに全てのユーザー情報と投稿情報を取得することができます。

JOIN (SQL文)

JOINは、2つ以上のテーブルを結合して、一つのテーブルのように扱うことができます。

Active RecordのクエリメソッドであるincludesメソッドではJOINが用いられています。

PostとUserのレコードをJOINするSQL文の例です。

SELECT * FROM posts
JOIN users ON posts.user_id = users.id;

Postモデルのテーブルであるpostsテーブルと、UserモデルのテーブルであるusersテーブルをJOINしています。

上記のSQL文によって取得される結果の例です。

[
  {
    id: 1,
    title: "Post 1",
    body: "Body 1",
    user_id: 1,
    created_at: "2023-04-01 00:00:00",
    updated_at: "2023-04-01 00:00:00",
    id: 1,
    name: "User 1",
    created_at: "2023-04-01 00:00:00",
    updated_at: "2023-04-01 00:00:00"
  },
  {
    id: 2,
    title: "Post 2",
    body: "Body 2",
    user_id: 2,
    created_at: "2023-04-01 00:00:00",
    updated_at: "2023-04-01 00:00:00",
    id: 2,
    name: "User 2",
    created_at: "2023-04-01 00:00:00",
    updated_at: "2023-04-01 00:00:00"
  }、
  {
    id: 3,
    title: "Post 3",
    body: "Body 3",
    user_id: 1,
    created_at: "2023-04-01 00:00:00",
    updated_at: "2023-04-01 00:00:00",
    id: 1,
    name: "User 1",
    created_at: "2023-04-01 00:00:00",
    updated_at: "2023-04-01 00:00:00"
  }
]

JOINによって、postsテーブルとusersテーブルのレコードを結合した結果が取得されています。

また、レコードの重複も解消されてUserモデルの情報を含んだPostモデルのレコードが取得されています。

includeメソッドとJOINの関係性についても深ぼってみました。

Active Recordはやっぱり便利ですね。

おわりに

コメント

本記事の内容は以上になります!

とても初歩的な内容でしたが、発行されるクエリや生成されるオブジェクトまで頭にいれておけると良いと思いました。

少しでも参考になったり学びのきっかけになりますと幸いです。

間違いがありましたら修正いたしますので、ご指摘ください。

興味があれば他の記事も更新していきますので是非ご覧になってください♪


◇ プログラミングスクールのご紹介 (卒業生より)

お世話になったプログラミングスクールであるRUNTEQです♪

https://runteq.jp/r/ohtFwbjW

ご不明な点ありましたらお気軽にコメントか、TwitterのDMでお答えします♪

https://twitter.com/outputky

参考