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.
精彩评论