はじめに
Rails で利用されるデザインパターンの1つに Query オブジェクトがあります。
複雑な ActiveRecord モデルの絞り込みを簡単なクラスに閉じ込めることで、可読性と疎結合を保つことができます。
Query オブジェクトを Relation に対し結合や絞り込み、ソートなどの操作を定義し、Relation を返すクラスとして定義し、
モデルの scope として利用することで、メソッドチェーンが利用できるようになります。
動作環境
Rails 6.1.3
Ruby 2.7.4
前提
レビューに対する通報機能があり、アイテムの詳細ページでアイテムに紐づいたレビューを一覧表示しています。
そして、レビューに対して通報がすると表示がされないように、自分が通報していないレビューの一覧を取得します。
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 クラスを定義します。
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
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 のフットモデルやフットコントローラーの問題を解決できます。