RailsのデザインパターンのひとつにQueryオブジェクトがあります。これはコントローラからActiveRecordモデルに対する絞り込みなどの操作を、ひとつの責務としてクラスに切り出すパターンです。コントローラの肥大化を防ぎ、またテストが書きやすくなります。このQueryオブジェクトについて示します。
Ruby on Railsアプリケーションの開発や学習に役立つ本を、「Ruby on Railsのおすすめな本」で紹介しています。あわせてご覧ください。
テクニカルライター。元エンジニア。共著で「現場で使えるRuby on Rails 5」を書きました。プログラミング教室を作るのが目標です。
Queryオブジェクトとは
以下、ActiveRecord::Relationを単にRelationと表記します。Queryオブジェクトは「Relationに対し結合や絞り込み、ソートなどの操作を定義し、Relationを返すクラス」です。ActiveRecordモデルのscopeとして設定することで、チェーンの一部として使用できるようになります。
よく見られるQueryオブジェクトの例として、Relationではなく配列などほかのクラスを返すものがあります。ただ、クラスやデザインパターンからはインタフェースや返り値が予想できるべきです。「QueryオブジェクトはRelationを返す」というルールのもとに設計することで、コードの品質が保たれることにつながります。
Queryオブジェクトの必要性
Queryオブジェクトは「コントローラからクエリ操作の責務を分離し、ActiveRecordモデルと疎結合に保つため」に必要なデザインパターンです。
コントローラからクエリを操作する場合、複雑なクエリは長くなり、肥大化してしまいます。また正しいRelationが取得できているかのテストが書きづらくなります。再利用性もありません。
たとえば「1日以内に記事を投稿したユーザーの一覧」を取得するコードを見てみます。コントローラに書くと、次のようになります。
class UsersController < ApplicationController def index @users = User.joins(:posts) .where( posts: { published_at: 1.day.ago.. } ) .order(created_at: :desc) end end
この例ならまだいいですが、これに「PVが100以上の記事を書いたユーザー」「フォロワーが3人以上ついたユーザー」のように条件がふえていくと、コントローラがどんどん肥大化していきます。
コントローラの責務はモデル層に命令を出し、ビュー層にデータを渡すことにあります。モデル層の操作を組み立てることではありません。コントローラがモデル層の知識を知りすぎると、モデル層の変更の影響を受けてしまいます。
Queryオブジェクトは、コントローラからクエリ操作の責務を分離し、クラス間を疎結合に保つためのデザインパターンです。
Queryオブジェクトの例
ここでは、上述したユーザー一覧の例をQueryオブジェクトで書いてみます。
動作環境
この記事にあるコードは次の各バージョンで動作を確認しています。
名前 | バージョン |
---|---|
Ruby | 2.7.1 |
Ruby on Rails | 6.0.3.2 |
コントローラ
まず、Queryオブジェクトの使われ方を把握するために、コントローラを見てみます。コントローラからは次のようにActiveRecordモデルのscopeとして呼び出します。引数として日付を指定しています。
# app/controllers/users_controller.rb class UsersController < ApplicationController def index @users = User.recently_posted(3.days) end end
ActiveRecordモデル
次にActiveRecordモデルです。scopeとしてQueryオブジェクトを渡しています。AcitveRecordモデルのscopeにすることで、「返ってくるのがそのモデルのRelationである」ことが自明になるというメリットがあります。
# app/models/user.rb class User < ApplicationRecord scope :recently_posted, RecentlyPostedUsersQuery end
Queryオブジェクト
最後にQueryオブジェクトです。QueryオブジェクトのベースとなるQueryクラスと、それを継承したRecentlyPostedUsersQueryクラスを定義しています。
ActiveRecordモデルのscopeとしてオブジェクトを渡すと、そのオブジェクトの#call
メソッドが呼ばれます。これを#new
に委譲することで、Queryオブジェクトの#call
が実行される仕組みです。#call
に引数を定義することで、コントローラから渡せるようになります。
# app/queries/query.rb class Query class << self delegate :call, to: :new end def call raise NotImplementedError end private attr_reader :relation end # app/queries/recently_posted_users_query.rb class RecentlyPostedUsersQuery < Query DEFAULT_FROM = 1.day def initialize(relation = User.all) @relation = relation end def call(from = DEFAULT_FROM) relation .joins(:posts) .where( posts: { updated_at: from.ago.. } ) end end
以上となります。コントローラからRelationに対する操作の責務をひとつのクラスに分離できました。ActiveRecordモデルに変更があってもコントローラへの影響がなくなり、またテストも書きやすくなりました。
Queryオブジェクトのルール
以上の内容をもとに、Queryオブジェクトを設計するときのルールについてまとめます。
- ファイルは
app/queries
に配置する - ベースとなるQueryクラスを定義する
- 接尾辞にQueryをつける
- クラス名から返るRelationが予想できる名前にする
#call
を定義し、ActiveRecord::Relationクラスのオブジェクトを返す- ActiveRecordモデルのscopeをとおしてのみ使用する。これにより、返るオブジェクトのクラスが自明となる
#call
は副作用のないメソッドにする
アンチパターン
Queryオブジェクトの例として、ひとつのクラスに複数のpublicメソッドを定義する例を見かけます。これはひとつのクラスに複数の責務をもつことになります。ある責務の変更が別の責務のメソッドに影響を及ぼすため、避けるべきだと考えます。
ひとつの責務をひとつのクラスに切り出し、単一責任の原則を守って設計することで、保守しやすいコードにすることができます。