开发者

Hacking ActiveRecord: add global named scope

I am trying to have a pack of very generic named scopes for ActiveRecord models like this one:

module Scopes
  def self.included(base)
    base.class_eval do
      named_scope :not_older_than, lambda {|interval|
        {:conditions => ["#{table_name}.created_at >= ?", interval.ago]
      }
    end
  end
end
ActiveRecord::Base.send(:include, Scopes)

class User < ActiveRecord::Base
end

If the named scope should be general, we need to specify *table_name* to prevent naming problems if their is joins that came from other chained named scope.

The problem is that we can't get table_name because it is called on ActiveRecord::Base rather then on User.

User.not_older_than(1.week)

NoMethodError: undefined method `abstract_class?' for Object:Class
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/base.rb:2207:in `class_of_active_record_descendant'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/base.rb:1462:in `base_class'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/base.rb:1138:in `reset_table_name'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/base.rb:1134:in `table_name'
from /home/bogdan/makabu/railsware/startwire/repository/lib/core_ext/active_record/base.rb:15:in `included'
from /var/lib/ge开发者_高级运维ms/1.8/gems/activerecord-2.3.5/lib/active_record/named_scope.rb:92:in `call'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/named_scope.rb:92:in `named_scope'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/named_scope.rb:97:in `call'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/named_scope.rb:97:in `not_older_than'

How can I get actual table_name at Scopes module?


Try using the #scoped method inside a class method of ActiveRecord::Base. This should work:

module Scopes
  def self.included(base)
    base.class_eval do
      def self.not_older_than(interval)
        scoped(:conditions => ["#{table_name}.created_at > ?", interval.ago])
      end
    end
  end
end

ActiveRecord::Base.send(:include, Scopes)


Rails 5, ApplicationRecord (Hope it helps others)

# app/models/concerns/not_older_than.rb

module NotOlderThan
  extend ActiveSupport::Concern

  included do
    scope :not_older_than, -> (time, table = self.table_name){ 
      where("#{table}.created_at >= ?", time.ago)
    }
  end
end

# app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  include NotOlderThan
end

# app/models/user.rb
class User < ApplicationRecord
  # Code
end

# Usage
User.not_older_than(1.week)

In Rails 5, all models are inherited from ApplicationRecord by default. If you wan to apply this scope for only particular set of models, add include statements only to those model classes. This works for join queries and chained scopes as well.


Additional useful scopes below :

module Scopes
  def self.included(base)
    base.class_eval do
      def self.created(date_start, date_end = nil)
          if date_start && date_end
            scoped(:conditions => ["#{table_name}.created_at >= ? AND #{table_name}.created_at <= ?", date_start, date_end])
          elsif date_start
            scoped(:conditions => ["#{table_name}.created_at >= ?", date_start])
          end
      end
      def self.updated(date_start, date_end = nil)
          if date_start && date_end
            scoped(:conditions => ["#{table_name}.updated_at >= ? AND #{table_name}.updated_at <= ?", date_start, date_end])
          elsif date_start
            scoped(:conditions => ["#{table_name}.updated_at >= ?", date_start])
          end
      end
    end
  end
end

ActiveRecord::Base.send(:include, Scopes)


Here is an updated, Rails4 compatible solution.
I am told defining global scopes like this can lead to conflicts, caveat emptor and all that, but sometimes you just need a simple scope on all your models, right?

Define a module.

# in /app/models/concerns/global_scopes.rb
module GlobalScopes
  def self.included(base)
    base.class_eval do
      def self.in_daterange(start_date, end_date)
        all.where(created_at: start_date.to_date.beginning_of_day..end_date.to_date.end_of_day)
      end
    end
  end
end

Have the module included in ActiveRecord::Base.

# in /config/initializers/activerecord.rb
ActiveRecord::Base.send(:include, GlobalScopes)

That's it! Notice that in Rails4 you do not have to mess with :scoped, but instead you use :all and chain your query to it.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜