Ruby
Rails

scope を ActiveSupport::Concern を利用して共通化する

More than 3 years have passed since last update.


概要

例えば User という Model に対してアソシエーション (has_many users あるいは belongs_to user) を持つ Model が複数存在するケースを想定します。

この場合、関連する User によってレコードを絞り込みたいときなどに by_users(users), by_user(user) のような同じフィルタ系の scope が重複することがしばしばあると思います。

これを防ぐために scope の定義を concerns に切り出してみました。


例1


app/models/concerns/related_to_user.rb

module RelatedToUser

extend ActiveSupport::Concern

# user もしくは users というアソシエーションを利用して
# users テーブルと INNER JOIN する。
joins_users = lambda do |relation|
association =
relation.reflect_on_all_associations
.map(&:name)
.find { |a| a.match(/\Ausers?\z/) }

break nil if association.nil?

relation.joins(association)
end

included do
scope :by_user, (lambda do |user|
joins_users.call(self)
.merge(User.where(id: user.id))
end)

scope :by_users, (lambda do |users|
user_ids =
if users.respond_to?(:pluck)
users.pluck(:id)
else
Array(users).map(&:id)
end

joins_users.call(self)
.merge(User.where(id: user_ids))
.uniq
end)
end
end



app/models/order.rb

class Order < ActiveRecord::Base

include RelatedToUser

belongs_to :user
end



app/models/item.rb

class Item < ActiveRecord::Base

include RelatedToUser

has_many :user_items, dependent: :destroy
has_many :users, through: :user_items
end



例2 より汎用的な方法 (追記)

もっと汎用的に使えるようにしました。


app/models/concerns/scope_filter.rb

module ScopeFilter

extend ActiveSupport::Concern

module ClassMethods
def scope_filter_by(association_name)
reflection = reflect_on_association(association_name)
return nil unless reflection

class_eval do
scope "by_#{association_name}", (lambda do |record_or_records|
relation = joins(association_name)

id_or_ids =
if record_or_records.is_a?(ActiveRecord::Base)
record_or_records.id
elsif record_or_records.respond_to?(:pluck)
record_or_records.pluck(:id)
else
Array(record_or_records).map(&:id)
end

relation.merge(reflection.klass.where(id: id_or_ids))
end)
end
end
end
end



app/models/order.rb

class Order < ActiveRecord::Base

include RelatedToUser

belongs_to :user
# by_user(user_or_users) という scope が定義される。
scope_filter_by :user
end



app/models/item.rb

class Item < ActiveRecord::Base

include RelatedToUser

has_many :user_items, dependent: :destroy
has_many :users, through: :user_items
# by_users(user_or_users) という scope が定義される。
scope_filter_by :users
end



お願い

まだまだ模索中ですので、もし他にいい方法、もしくは似たようなことを実現するための Gem 等があればぜひ教えて下さい。