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メソッドを定義する例を見かけます。これはひとつのクラスに複数の責務をもつことになります。ある責務の変更が別の責務のメソッドに影響を及ぼすため、避けるべきだと考えます。
ひとつの責務をひとつのクラスに切り出し、単一責任の原則を守って設計することで、保守しやすいコードにすることができます。