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

9 minute read

高尾が解説する 詳解Railsデザインパターン・シリーズの「Queryオブジェクト」編です。

他のデザインパターンも解説していますので、よろしければご覧ください。


Rails の Queryオブジェクト パターンは scope を別ファイルに定義することでモデルの肥大化を防ぐことに効果があるデザインパターンです。

今回はこの Queryオブジェクト パターン を扱います。

Google広告


関連記事・レポジトリ

まずは関連記事の紹介。これらを読めば Queryオブジェクト パターンを理解できます。

解説付きの実装例は pattern/query.rb at master · Selleo/pattern があります。このレポジトリにあるコードはシンプルでいいですね。

なお、前述した Rails - ActiveRecord の scope を Query object で実装する - Qiita の後続記事として 【Rails】Finder Object で検索ロジックをすっきりさせる - furaji |> exists? があります。 scope に対する Query オブジェクトは使わなくなり、最近は Finder オブジェクトを使っている とのこと。 (次は Finder オブジェクトを解説したいな)

Selleo/pattern を使って Query オブジェクトを定義

pattern/query.rb at master · Selleo/pattern を利用し、解説 に従って queries に対象のモデルまたはリレーション、プライベートメソッドとして query を定義すればOKです。このクラスは app/queries/recently_activated_users_query.rb に配置します。とても簡単ですね。

class RecentlyActivatedUsersQuery < Patterns::Query
  queries User

  private

  def query
    relation.active.where(activated_at: date_range)
  end

  def date_range
    options.fetch(:date_range, default_date_range)
  end

  def default_date_range
    Date.yesterday.beginning_of_day..Date.today.end_of_day
  end
end

使い方 も簡単です。scope での使い方も解説されていてありがたいです。

RecentlyActivatedUsersQuery.call
RecentlyActivatedUsersQuery.call(User.without_test_users)
RecentlyActivatedUsersQuery.call(date_range: Date.today.beginning_of_day..Date.today.end_of_day)
RecentlyActivatedUsersQuery.call(User.without_test_users, date_range: Date.today.beginning_of_day..Date.today.end_of_day)

class User < ApplicationRecord
  scope :recently_activated, RecentlyActivatedUsersQuery
end

Query オブジェクトのベースクラスの実装

せっかくなので、 pattern/query.rb at master · Selleo/pattern を参考にして自作します。

というのも、私は最近「Rails のアプリケーションをメンテナンスし続けるには、利用する gem を最小限にして、できれば Rails のみで完結したい」と考えるようになり、 Selleo/pattern のようなシンプルなものは gem ではなく、最小限の実装をアプリケーションに取り込んだほうが良い、と考えているからです。

gem のアップデートはそれなりにコストがかかるんですよね。たびたびメンテナンスされなくなったり、Railsのバージョンアップで対応できなくなったり。オープンソースソフトウェアなので困ったら自分で直せばいいのですが、いつまでもそのアプリケーションの開発に携われるわけでもなかったりしてね。

というわけで、以下が最小限の実装です。クラス名は ApplicationController や ApplicationRecord に従って ApplicationQuery にしました。とてもシンプルですね。これだけで pattern/query.rb at master · Selleo/pattern を実現できます。

app/queries/application_query.rb

require "active_record"

# The Query object pattern base class
#
# based on https://github.com/Selleo/pattern/blob/master/lib/patterns/query.rb
# MIT License: https://github.com/Selleo/pattern/blob/master/LICENSE.txt
class ApplicationQuery
  class << self
    attr_accessor :base_relation

    def queries(subject)
      self.base_relation = subject
    end
    
    def call(*args)
      new(*args).send(:query)
    end
  end

  def initialize(*args)
    @options = args.extract_options!
    @relation = args.first || base_relation
  end

  private

  attr_reader :relation, :options

  def base_relation
    return self.class.base_relation if self.class.base_relation.is_a?(ActiveRecord::Relation)

    self.class.base_relation.all
  end
  
  # :nocov:
  def query
    raise NotImplementedError, "You need to implement #query method which returns ActiveRecord::Relation object"
  end
  # :nocov:
end

まとめ

  • Queryオブジェクト パターンは scope を別ファイルに定義することでモデルの肥大化を防ぐことに効果があるデザインパターン
  • pattern/query.rb at master · Selleo/pattern を使えばOK
  • Queryオブジェクトのベースクラスは簡単に自作できる

タグ: ,

カテゴリー:

更新日時: