Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Railsの技: Active Recordオブジェクトはチェイン可能にして返そう(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

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のsortselectといった配列メソッドに切り替えなければならず、より多くのデータが必要になると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は空の結果を返しますが、この結果はチェイン可能です。この空のリレーションに対してorderincludeswhereなどの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の技: パンくずリストをgemなしで実装する(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。