Edited at

ActiveRecordで動的なjoinをスッキリ書く方法

More than 1 year has passed since last update.

エイチームライフスタイルアドベントカレンダー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|

books.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/