55
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Ateam LifestyleAdvent Calendar 2017

Day 1

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

Last updated at Posted at 2017-11-30

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

55
20
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
55
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?