キャッシュが効かないとき
# app/model/category.rb
class Category < ApplicationRecord
has_many :books
end
# app/model/book
class Book < ApplicationRecord
scope :published, -> { where(published: true) }
belongs_to :category
end
# app/controller/books_controller
def index
@category = Category.All.includes(:books)
end
# app/views/books/index.html.slim
- @categories.each do |category|
h1
= category.name
table
- category.books.published.each do |book|
tr
td = book.name
td = book.author
このように、せっかくincludes(:book)
をしても、category.books.published
や、category.books.where(published: true)``category.books.order_by(id: :desc)
のように、子のモデルにクエリを付け足してしまうとキャッシュが効かず、N+1問題が発生してしまう。
どうするか
# app/model/category.rb
class Category < ApplicationRecord
has_many :books
has_many :published_books, -> { published }, class_name: 'Book'
end
# app/model/book
class Book < ApplicationRecord
scope :published, -> { where(published: true) }
belongs_to :category
end
# app/controller/books_controller
def index
@category = Category.All.includes(:published_books)
end
# app/views/books/index.html.slim
- @categories.each do |category|
h1
= category.name
table
- category.published_books.each do |book|
tr
td = book.name
td = book.author
has_many :published_books, -> { published }, class_name: 'Book'
のように、has_manyにスコープを渡してやり、それをincludes
の引数に渡すと、キャッシュが効いてくれる。
備考
ActiveRecord::QueryMethodsのコメントには、
# === conditions
#
# If you want to add conditions to your included models you'll have
# to explicitly reference them. For example:
#
# User.includes(:posts).where('posts.name = ?', 'example')
#
# Will throw an error, but this will work:
#
# User.includes(:posts).where('posts.name = ?', 'example').references(:posts)
#
# Note that +includes+ works with association names while +references+ needs
# the actual table name.
とあるので、
Category.includes(:books).merge(Book.published).references(:books)
としてもよい。
なお、どちらの場合においても、Bookのスコープの条件が、親のモデル(Category)になるため、上の例ではpublishedなbookが一つもないcategoryはクエリの結果として出てこないので注意。