概要
例えば 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 等があればぜひ教えて下さい。