概要
タイトルの通り、「includesはN+1を解決してくれるjoinsのようなもの」という認識だったが、
実はそんな単純な話では無かった・・・という話。
(そんな事くらい、あらかじめ理解しておけって言われそうだけども)
状況説明
- Project
- Genre
- User
という3つのモデルがあり、
ProjectとGenre
、UserとGenre
にそれぞれ中間テーブルを設けています。
用途は、「この案件(project)の種類(genre)は"AとB"として登録」
「このユーザー(user)は自分の種類(genre)を登録し、該当するものを対応」みたいな使われ方を想定している。


class Project < ApplicationRecord
has_and_belongs_to_many :genres
end
class Genre < ApplicationRecord
has_and_belongs_to_many :users
has_and_belongs_to_many :projects
end
class User < ApplicationRecord
has_and_belongs_to_many :genres
期待していた動作
ここで、projectsの案件1〜6のgenreは以下であるとし、
:name | :genres |
---|---|
案件1 | A,B,C |
案件2 | B,C,D |
案件3 | A |
案件4 | C |
案件5 | C,E |
案件6 | D,E |
current_userのgenresが**"A"と"B"と"C"**の場合(current_user.genres.pluck(:name) #=> ["A", "B", "C"]
)、
current_userのproject一覧で以下のような一覧が表示される事を期待していた。
つまり、「自分の属するgenreが含まれる案件(project)が一覧表示」され、ジャンル列には各案件の属するgenreが表示されることを期待する。
(↑案件6はDとEなので、表示されない)
実装したコードを簡単に説明
ProjectからUserまでの関係はそれぞれの中間テーブルを挟みながら
Project -- Genre -- User
数珠つなぎであることと、N+1問題
を考慮し、project.rb内でincludes
を用いて以下のscopeを作成した。
scope :search_by_user, ->(user) {
includes(genres: :users).
where('users.id' => user.id).distinct
}
controllerのindexで、↑の:search_by_user
scopeを使用。
def index
@projects = Project.search_by_user(current_user)
end
viewは↓(一部)
<% @projects.each do |project| %>
<tr>
<td><%= project.name %></t>
<td><%= project.created_at.to_date.to_s.gsub(/-/, ".") %></td>
<td><%= project.genres.pluck(:name).join(" , ") %></td>
意外な結果に...
期待と外れて↓が表示されてしまった。

ちなみに、current_userの属するgenreを**"A"と"B"**にすると以下のようになる。↓
各案件のジャンル列で、自分が属するジャンルのものしか表示されなくなってしまった。
joinsで解決
:scope
のinclude
を joins
に替えると期待通りの表示となった。
scope :search_by_user, ->(user) {
joins(genres: :users).
where('users.id' => user.id).distinct
}
しかし・・・
includesでN+1問題
が解決されていたところ、joinsに替えてしまったので、
SQLクエリがN+1
の状態となってしまった。
表示が期待通りとなり、N+1問題も解決する方法は無いものだろうか・・・