Railsで重要なパターンpart 2: Query Object(翻訳)

概要

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

Railsで重要なパターンpart 2: Query Object(翻訳)

Query Object(または単にQuery)パターンもまた、Ruby on Rails開発者が肥大化したActiveRecordモデルを分割し、コントローラをスリムで読みやすくするのに非常に有用なパターンです。本記事はRuby on Railsを念頭に置いていますが、このパターンは他のフレームワーク(特にMVCベースでActiveRecordパターンを適用できるもの)にも簡単に適用できます。

どんなときにQuery Objectパターンを使うか

ActiveRecordリレーションで実行しなければならないクエリが複雑になったら、Query Objectパターンの利用を検討すべきです。スコープをこの目的に使うことはおすすめできません。

目安として、スコープが複数のカラムとやり取りする場合や、他のテーブルとJOINする場合は、Query Objectへの移行を検討すべきです。これにより、モデルに定義するスコープの数を必要最小限に減らすという副次的効果も得られます。同様に、スコープのチェインを扱う場合は常にQuery Objectの利用を検討すべきです(関連記事)。

Query Objectパターンを最大限に利用するための注意点

1. 命名規則を定める

素晴らしいQuery Objectクラスに楽に名前を付けられるよう、基本的な命名規則をいくつか定めましょう。規則のひとつとして考えられるのは、Queryオブジェクト名の末尾にQueryを追加することです。こうすることで今扱っているものがActiveRecordの子孫ではなくQueryであることを常に意識できます。
その他に、モデル名を複数形にして、Queryがどのオブジェクトと協調動作するよう設計されているかを示す方法も考えられます。たとえばRecentProjectUsersQueryというQuery Objectは、呼び出されるとUserのリレーションを返すことが明確にわかります。どの規則を選ぶにしても、パターンに基づいたクラスの命名法が一貫すれば新規導入クラスの命名に迷う時間を減らせるので、メリットを得られることが多くなります。

2. リレーションを返す.callメソッドをQuery Objectの呼び出しに使う

Service Objectでは、Service Objectを使う専用メソッドの命名方法にある程度選択の余地がありますが、対照的に、RailsでQuery Objectパターンを最大限に活用するには、リレーションオブジェクトを返す.callメソッドを実装すべきです。この規則に従うことで、必要に応じてQuery Objectで簡単にスコープを構成できるようになります(関連記事)。

3. オブジェクトなどのリレーションは常に第1引数で受け取る

導入するQuery Objectの呼び出しでは、第1引数でリレーションを受け取るのがよい方法です。Query Objectをスコープとして利用するときに第1引数のリレーションが必須(2.の推奨事項を参照)になりますし、Query Objectをチェインできるので柔軟性も高まります。Query Objectの使いやすさを損なわないためには、デフォルトのエントリリレーションを設定して、引数なしでもQuery Objectを利用できるようにしましょう。また、リレーションQuery Objectが提供されたときと同じ主題(テーブル)を持つQuery Objectから常にリレーションを返すことも重要です。

4. 追加オプションを受け取れるようにする

追加オプション受け取りの必要性は、既存のQuery Objectや新規Query Objectの導入時にサブクラス化することである程度回避できますが、いずれQuery Objectで追加オプションを受け取る必要が生じます。Query Objectで追加オプションを受け取れるようにしておけば、結果をどのように返すかというロジックをカスタマイズできるので、Query Objectを柔軟なフィルタとして効果的に利用できます。コードが読みにくくならないよう、追加オプションは必ずハッシュまたはキーワード引数として渡し、デフォルト値も設定しておくことをおすすめします。

5. 読みやすいクエリメソッドを書くことに集中する

Queryのコアロジックを.callメソッド自身の中に保存する場合であっても、Query Objectの別のメソッドに保存する場合であっても、常に読みやすさを心がけるべきです。他の開発者はQuery Objectの意図を確認する際にクエリメソッドを調べるので、クエリメソッドで少し手間をかけておけばQuery Objectが活用しやすくなります。

6. Query Objectを名前空間でグループ化する

プロジェクトの複雑さや、ActiveRecordをどの程度利用するかによって多少異なりますが、いずれQuery Objectはどんどん増えていきます。コードを整理するよい方法のひとつは、類似したQuery Objectを名前空間でグループ化することです。Queryが扱うモデルの名前でグループ化しても構いませんし、十分な理由付けがなされていれば何を使っても構いません。これまでと同様、Query Objectのグループ化方法も1つに決めておくことで、新規導入するクラスの適切な配置が楽に決まります。Query Objectをすべてapp/queriesディレクトリに保存する方法もおすすめです。

7. すべてのメソッドを.callの結果に委譲することも検討

Query Object用のmethod_missingを実装して全メソッドを.callメソッドの結果に委譲する方法も考えられます。この方法の場合、Query Objectは単に通常のリレーションとして用いられます(例: RecentProjectUsersQuery.call.where(first_name: “Tony”)ではなくRecentProjectUsersQuery.where(first_name: “Tony”)になる)。しかし、メタプログラミングと同様、この方法を選ぶ際には十分な検討と理由付けを行うべきです。

まとめ

Query Objectパターンは、実装の複雑なクエリ/リレーション/スコープを抽象化できるシンプルなパターンであり、テストも簡単になります。上述のシンプルな規則に従うことで、可読性や柔軟性を失わずにこのパターンを簡単に利用できるようになります。開発者自身はもちろん、何より将来そのコードを使う他の開発者にとってメリットになります。そのようなQuery Objectの実装例を以下に示します。

module Users
  class WithRecentlyCreatedProjectQuery
    DEFAULT_RANGE = 2.days

    def self.call(relation = User.all, time_range: DEFAULT_RANGE)
      relation.
        joins(:projects).
        where('projects.created_at > ?', time_range.ago).
        distinct
    end
  end
end

Query Objectパターンをシンプルに抽象化したい場合は、rails-patterns gemが提供するラッパーの導入をご検討ください。

関連記事

Railsで重要なパターンpart 1: Service Object(翻訳)

Railsの`CurrentAttributes`は有害である(翻訳)

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

リファクタリングRuby: サブクラスをレジストリに置き換える(翻訳)

Ruby on RailsによるWEBシステム開発、Android/iPhoneアプリ開発、電子書籍配信のことならお任せください この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

BigBinary記事より

ActiveSupport探訪シリーズ