Railsの技: Active Recordオブジェクトはチェイン可能にして返そう(翻訳)
Active Recordで優秀な点のひとつは、クエリインターフェイスが以下のようにチェイン可能(chainable)である点です。
Post.includes(:comments)
.where(published: true)
.where(author: Current.user)
.order(:name)
この強みを活用してコードを柔軟にするために、データにクエリをかけるときは常にチェイン可能なオブジェクトを返すようにしましょう。
使い方
アプリケーションが育つに連れて、複雑なクエリからデータを切り出すようになりがちです。
class SpecialOffer
def self.find_eligible_products(store, shopper)
return [] if store.restricted?
store.products
.where('price >= ?', 100)
.select{ |p| shopper.can_order?(p) }
end
end
@products = SpecialOffer.find_eligible_products(store, shopper)
#=> [ #<Product:0x00007fb1719b7ec0>, #<Product:0x00007fb174744de8>, ... ]
上のコードはとりあえず動くかもしれませんが、今後@products
を何らかの方法でorder
する必要が生じたらどうなるでしょうか?ロジックを追加したらどうなるでしょうか?何らかの関連付けをlazy loadingするとどうなるでしょうか?
この場合、SpecialOffer
のメソッドが返しているのは配列型です。これではRubyのsort
やselect
といった配列メソッドに切り替えなければならず、より多くのデータが必要になるとN+1クエリバグにつながる可能性もあります。
このコードを以下のようにリファクタリングして、チェイン可能なオブジェクトを返すようにしましょう。
class SpecialOffer
def self.find_eligible_products(store, shopper)
return Product.none if store.restricted?
product_ids = store.products
.where('price >= ?', 100)
.select{ |p| shopper.can_order?(p) }
.map(&:id)
Product.where(id: product_ids)
end
end
@products = SpecialOffer.find_eligible_products(store, shopper)
#=> Product::ActiveRecord_Relation
最初にnone
というクエリメソッドを用いています。none
は空の結果を返しますが、この結果はチェイン可能です。この空のリレーションに対してorder
やincludes
やwhere
などのActive Recordメソッドを呼び出すと、単に空の結果を返します。
次に、productの複雑なクエリを直接返す代わりに、該当するproductをコレクションし、「改めて」productのid
に対応する結果を返します。この場合データベースへのクエリが追加で発生しますが、必要に応じて結果を自由に操作することもできます。
結果をソートしたい場合や関連付けを読み込みたい場合は、そうした操作をデータベース内で行えるようになり、処理の一部として実行された既存の条件を気にする必要がなくなります。
@products = SpecialOffer.find_eligible_products(store, shopper)
.includes(:variants)
.order(:price)
@products = SpecialOffer.find_eligible_products(store, shopper)
.joins(:sales)
.where("sales.count > 15")
.order(:sku)
このパターンは、データを適切な形に加工するための柔軟性を損なわずに複雑なクエリを取り出せるので、非常に重宝することに気が付きました。
参考資料
- Rails API:
ActiveRecord::QueryMethods#none
-
Railsガイド: Active Record クエリインターフェイス
概要
原著者の許諾を得て翻訳・公開いたします。