MAGAZINE

ルーターマガジン

web / UI

ActiveRecordのpreloadで結合先のテーブルで絞り込む方法

2024.08.23
Pocket

Railsの開発において、データベースから効率的にデータを取得するためにpreload、eager_load、includesなどのメソッドを活用することが一般的です。これらのメソッドにはそれぞれ異なる特性があり、状況に応じて使い分ける必要があります。今回は、preloadを用いて結合先のテーブルで絞り込みを行う方法について詳しく解説します。

1. preload、eager_load、includesの使い分け

まずは、preload、eager_load、includesの違いを簡単に整理しておきましょう。

  • preload:

    • 複数のクエリを発行し、メインのテーブルと関連するテーブルのデータをそれぞれ取得します。
    • preload自体では結合先のテーブルでの絞り込みは行えません。
  • eager_load:

    • 左外部結合(LEFT OUTER JOIN)を利用し、1つのクエリでメインのテーブルと関連するテーブルのデータを取得します。
    • 結合先のテーブルで絞り込みが可能です。
  • includes:

    • Railsが内部的にpreloadまたはeager_loadを選択して実行します。
    • includesにwhere条件を含めた場合、自動的にeager_loadが使用され、結合先のテーブルで絞り込みが行われます。

多くのブログでは、「結合先のテーブルで絞り込めるのがeager_loadincludes、絞り込めないのがpreload」と説明されています。 しかし、preloadを使いながら結合先のテーブルで絞り込みを行う方法も存在します。

2. 絞り込み条件を設定したアソシエーションを使ってpreloadする

preloadを利用する際、関連するテーブルとのアソシエーションが必ず定義されている必要があります。 アソシエーションには、where条件を設定することが可能なので、結合先のテーブルで絞り込みを行いたい場合、あらかじめ絞り込み条件を設定したアソシエーションを定義し、それをpreloadに使用します。具体的には以下のような手順になります。

モデルの定義

例えば、UserモデルとPostモデルがあり、Userがhas_many :postsという関係にあるとします。

class User < ApplicationRecord
  # 絞り込み条件なしのアソシエーション
  has_many :posts

  # 絞り込み条件付きのアソシエーション
  has_many :published_posts, -> { where(status: 'published') }, class_name: 'Post'
end

class Post < ApplicationRecord
  belongs_to :user
end

この例では、published_postsというアソシエーションを定義し、statusがpublishedのPostのみを取得するようにしています。

preloadで結合先のテーブルで絞り込めないケース

絞り込み条件なしのpostsをpreloadしてから、postsテーブルのカラムで絞り込むと、例外が発生して失敗します。

# 絞り込み条件なしでpreloadし、その後にwhereで絞り込みを試みる
users = User.preload(:posts).where(posts: { status: 'published' })

# 例外の発生
# User Load (19.5ms)  SELECT `users`.* FROM `users` WHERE `posts`.`status` = 'published'
# (Object doesn't support #inspect)

preloadは別のクエリとしてpostsを取得するため、where句での絞り込みはUserテーブルに対して行われ、関連するpostsのデータは絞り込まれません。

絞り込み条件なしのアソシエーションでのpreload

例えば、postsテーブルにレコードが3行存在し、そのうちid = 3のレコードだけstatusが"published"ではなく、"draft"である場合に、絞り込み条件なしのアソシエーションであるpostsをpreloadすると、statusに関係なくすべてのレコードが参照されます。

# 絞り込み条件なしでpreload
users = User.preload(:posts)

# 実行されるクエリ
# SELECT `users`.* FROM `users`
# SELECT `posts`.* FROM `posts` WHERE  `posts`.`id` IN (1, 2, 3)

# 出力結果
p users.first.posts.pluck(:id, :status)
# 出力例:
# [[1, "published"], [2, "published"], [3, "draft"]]

絞り込み条件付きのアソシエーションでのpreload

次に、絞り込み条件ありのアソシエーションであるpublished_postsをpreloadした場合は以下のようになります。preloadを使用しても、結合先のテーブルのstatusが"published"のレコードのみが絞り込まれて取得されます。

# 絞り込み条件付きでpreload
users = User.preload(:published_posts)

# 実行されるクエリ
# SELECT `users`.* FROM `users`
# SELECT `posts`.* FROM `posts` WHERE `posts`.`status` = 'published' AND `posts`.`id` IN (1, 2, 3)

# 出力結果
p users.first.published_posts.pluck(:id, :status)
# 出力例:
# [[1, "published"], [2, "published"]]

3. まとめ

今回紹介した方法は、ActiveRecordのpreloadを使って結合先のテーブルで絞り込みを行うテクニックです。preloadに利用するアソシエーションに事前に絞り込み条件を設定することで、preloadを使いながらも特定の条件に合致するレコードだけを取得できます。

eager_loadはデータサイズが大きいとレスポンスタイムが増加するため、preloadを使用しながら絞り込みを行うことで、より高いパフォーマンスを維持できます。

この方法を活用することで、preloadを使いつつ効率的にデータを取得し、アプリケーションのパフォーマンスを改善することができます。

Pocket

CONTACT

お問い合わせ・ご依頼はこちらから