エイチームライフスタイルアドベントカレンダー2017、1日目です!
初回は 株式会社エイチームライフスタイル のエンジニア @u_minor3110 が担当します。
今日はActiveRecordのお話をします。
さっそくですが、SQLでは簡単に書けても、ActiveRecordではどうやって書くんだろう?と悩んだことはありませんか?Arelを使うにしても、可読性が下がってしまう気がするし、N+1を回避するためにpreloadさせたい、なんて時はさらに書き方に困ります。
今回は join時に動的に結合条件を追加したい、かつ、preloadさせたい 時の解決方法です。
たとえば以下の情報が取得したい時。
「本の一覧を特定の日付以降のレビューがあればそれを添えて表示させる」
SELECT * FROM books b LEFT JOIN reviews r ON b.id = r.book_id AND r.created_at >= 'xxx'
SQLで書くと一例としてこうなります。
ActiveRecordで書くなら、まず思いついた書き方は、
Book.eager_load(:reviews).where('reviews.created_at >= ?', 'xxx')
これだと、レビューのない本が表示されません。
(join後にwhereが適用されるため。)
Book.eager_load(:reviews).where('reviews.created_at >= ? OR reviews.id IS NULL', 'xxx')
これだと、レビューのない本も表示されますが、レビューの日付がxxxより古い本が表示されません。
associationに動的に条件をつけられるようにしてみる
次に思いついた書き方。
class Book < ApplicationRecord
cattr_accessor :recent_created_at
has_many :recent_reviews, lambda { where('created_at >= ?', @@recent_created_at) }, class_name: 'Review'
end
Book.recent_created_at = 'xxx'
Book.eager_load(:recent_reviews)
これだといちおう上手くいきます。ただ、クラス変数を使っている時点でスレッドセーフではありません。
変数をスレッドセーフに扱えるgemもありますが、秘伝のタレ化するのが見えています。
ggrks
次に検索して出てきたtipsを試してみる。
Book.eager_loads(:reviews).joins("and reviews.created_at >= 'xxx'")
いけます。上手くいきますが、joinsが複数あるとandの位置がjoinの最後に来てしまうためダメです。ゆえにオススメはできません。
別々にクエリを発行してみる
次に考えたのは、別々にクエリを発行する方法。
books = Book.all
reviews = Review.where(book_id: books.pluck(:id)).where('created_at >= ?', 'xxx').index_by(&:id)
books.each do |book|
reviews[book.id]
end
いけます!ただ、スマートではない気がしますね。
よくある処理だと思っていたので、簡単に対応できると思っていたのですが、こうなるといよいよArelの出番か、、、と思っていたのですが、よい方法が見つかりました。
Preloaderが鮮やかだった
books = Book.all
ActiveRecord::Associations::Preloader.new.preload(books, :reviews, Review.where('created_at >= ?', 'xxx'))
books.each do |book|
book.reviews
end
これなら通常のassociationのように扱えます。
実はこれ、半年くらい解決方法に悩んでいました。他に良い方法をご存知の方がいらっしゃったら教えてください。
参考
https://qiita.com/k0kubun/items/80c5a5494f53bb88dc58
https://kadoppe.com/archives/2012/01/dinamic_conditions_for_has_many_association.html
http://aserafin.pl/2017/09/12/preloading-associations-with-dynamic-condition-in-rails/
最後に
エイチームライフスタイルアドベントカレンダー2017の1日目、いかかでしたか。
まずは新しいことより、実用的なところをテーマにしてみました。
2日目は株式会社エイチームライフスタイルが誇る、インフラを中心に活躍する @ihsiekさんが Elasticsearch に関する記事を書かれるそうですので、お楽しみに!
株式会社エイチームライフスタイルでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。興味を持たれた方はぜひエイチームグループ採用サイトを御覧ください。
http://www.a-tm.co.jp/recruit/