LoginSignup
1
1

More than 1 year has passed since last update.

Railsのデザインパターン QueryObjectの作り方

Last updated at Posted at 2022-01-09

はじめに

Rails で利用されるデザインパターンの1つに Query オブジェクトがあります。
複雑な ActiveRecord モデルの絞り込みを簡単なクラスに閉じ込めることで、可読性と疎結合を保つことができます。
Query オブジェクトを Relation に対し結合や絞り込み、ソートなどの操作を定義し、Relation を返すクラスとして定義し、
モデルの scope として利用することで、メソッドチェーンが利用できるようになります。

動作環境

Rails 6.1.3
Ruby 2.7.4

前提

レビューに対する通報機能があり、アイテムの詳細ページでアイテムに紐づいたレビューを一覧表示しています。
そして、レビューに対して通報がすると表示がされないように、自分が通報していないレビューの一覧を取得します。

app/model/review.rb
class Review < ApplicationRecord
  belongs_to :product
  belongs_to :user
  has_many :reports, dependent: :destroy

  def self.unreported_reviews(product_id) #誰にも通報されていないレビュー
    eager_load(:reports).where(product_id: product_id, reports: { id: nil })
  end

  def self.reviews_reported_other(product_id, user_id) #自分以外が通報したレビュー
    eager_load(:reports).where(product_id: product_id).where.not(reports: { user_id: user_id })
  end

  def self.exclude_reported_reviews(product_id, user_id) #自分が通報していないレビュー
    self.unreported_reviews(product_id).or(self.reviews_reported_other(product_id, user_id))
  end
end

これだと1つのコントローラーでしか使われない複雑なメソッドができてしまいます。
ここにあるメソッドを Query オブジェクトとしてモデルから切り離します。

Qurey オブジェクト

モデルから切り離すため app ディレクトリの下に qurey ディレクトリを作成し、ベースとなる Qurey クラスを作成し、それを継承した ExcludeReportedReviewsQuery クラスを定義します。

app/query/query.eb
class Query
  class << self
    delegate :call, to: :new
  end

  def initialize(relation)
    @relation = relation
  end

  def call
    raise NotImplementedError
  end

  private

    attr_reader :relation
end

`ruby:app/query/exclude_reported_reviews_query.rb
class ExcludeReportedReviewsQuery < Query
def initialize(relation = Review.all)
super(relation)
@relation = relation
end

def call(product_id, user_id)
unreported_reviews(product_id).or(reviews_reported_other(product_id, user_id))
end

private

def unreported_reviews(product_id)
@relation.includes(:reports, :user, :product_review_likes).eager_load(:reports).where(product_id: product_id, reports: { id: nil })
end

def reviews_reported_other(product_id, user_id)
@relation.eager_load(:reports).where(product_id: product_id).where.not(reports: { user_id: user_id })
end
end

app/model/review.rb
class Review < ApplicationRecord
  belongs_to :product
  belongs_to :user
  has_many :reports, dependent: :destroy

  scope :exclude_reported_reviews, ->(product_id, user_id) { ExcludeReportedReviewsQuery.call(product_id, user_id) }
end

このように定義することで複雑なクエリをコントローラーやモデルから分離できました。
ActiveRecord モデルに変更があって影響が少なくなります。

まとめ

ひとつの責務をひとつのクラスに切り出し、単一責任の原則を守って設計することで、保守しやすいコードにできます。
このようなデザインパターンを使うことで Rails のフットモデルやフットコントローラーの問題を解決できます。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1