LoginSignup
17
17

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-02-09

概要

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

17
17
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
17
17