目次

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

本サイトはアフィリエイトプログラムを利用した広告を表示しています

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

Railsのおすすめな本

Ruby on Railsアプリケーションの開発や学習に役立つ本を、「Ruby on Railsのおすすめな本」で紹介しています。あわせてご覧ください。

著者
Hiroki Zenigami

テクニカルライター。元エンジニア。共著で「現場で使える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オブジェクトで書いてみます。

動作環境

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

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

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

あわせて読みたい

著者
Hiroki Zenigami

テクニカルライター。元エンジニア。共著で「現場で使えるRuby on Rails 5」を書きました。プログラミング教室を作るのが目標です。

スポンサーリンク

ブログをはじめよう

技術ブログの始め方を、たくさんの画像で分かりやすく解説しました。これまでブログをやったことがない人でも、エンジニアにとって重要なブログを今日から始められます。

ブログをはじめる