【问题标题】:Hacking ActiveRecord: add global named scopeHacking ActiveRecord:添加全局命名范围
【发布时间】:2010-09-30 09:02:38
【问题描述】:

我正在尝试为像这样的 ActiveRecord 模型提供一组非常通用的命名范围:

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

如果命名范围应该是通用的,我们需要指定 *table_name* 以防止它们是来自其他链式命名范围的连接时出现命名问题。

问题是我们无法获取 table_name,因为它是在 ActiveRecord::Base 而不是 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/gems/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'

如何在 Scopes 模块中获取实际的 table_name?

【问题讨论】:

    标签: ruby-on-rails ruby activerecord


    【解决方案1】:

    尝试在 ActiveRecord::Base 的类方法中使用#scoped 方法。这应该有效:

    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)
    

    【讨论】:

    • 关于如何为 default_scope 执行此操作的任何想法?我遇到了同样的问题,但我调用的是 default_scope 而不是 named_scope
    • 这是直接的钱。谢谢!
    【解决方案2】:

    Rails 5,ApplicationRecord(希望对其他人有所帮助)

    # 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)
    

    在 Rails 5 中,所有模型默认继承自 ApplicationRecord。如果您想将此范围仅应用于特定的模型集,请仅将包含语句添加到这些模型类。这也适用于连接查询和链式作用域。

    【讨论】:

    • 你也可以把它扔进module ClassMethods而不是included,因为scope(大部分)只是一种定义类方法的方式。
    • Scope 只是类方法的语法糖。我们可以将范围链接在一起,而不必担心如果我们添加条件检查会返回 nil 值。
    【解决方案3】:

    以下其他有用的范围:

    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)
    

    【讨论】:

      【解决方案4】:

      这是一个更新的、兼容 Rails4 的解决方案。
      有人告诉我,像这样定义全局范围可能会导致冲突,caveat emptor 等等,但有时您只需要在所有模型上设置一个简单的范围,对吧?

      定义一个模块。

      # 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
      

      将模块包含在ActiveRecord::Base中。

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

      就是这样!请注意,在 Rails4 中,您不必乱用 :scoped,而是使用 :all 并将查询链接到它。

      【讨论】:

        猜你喜欢
        • 2012-05-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-04-13
        • 2011-03-29
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多