Railsのデザインパターン: Queryオブジェクト

RailsのデザインパターンのひとつにQueryオブジェクトがあります。 これはコントローラからActiveRecordモデルに対する絞り込みなどの操作を、ひとつの責務としてクラスに切り出すパターンです。 コントローラの肥大化を防ぎ、またテストが書きやすくなります。 このQueryオブジェクトについて示します。

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オブジェクトで書いてみます。

動作環境

この記事にあるコードは次の各バージョンで動作を確認しています。

名前バージョン
Ruby2.7.1
Ruby on Rails6.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メソッドを定義する例を見かけます。 これはひとつのクラスに複数の責務をもつことになります。 ある責務の変更が別の責務のメソッドに影響を及ぼすため、避けるべきだと考えます。

ひとつの責務をひとつのクラスに切り出し、単一責任の原則を守って設計することで、保守しやすいコードにすることができます。


Author
著者Hiroki Zenigami

プロダクト開発者。妻と娘、猫とのんびり暮らしています。


Publishing
現場で使えるRuby on Rails 5

共著で「現場で使えるRuby on Rails 5(マイナビ出版)」を出版しました