扩展ActiveRecord::Base

9

我正在尝试为ActiveRecord添加一些自定义方法。我想为模型的每个date字段添加一个*_after*_before范围,以便我可以像这样做:

User.created_at_after(DateTime.now - 3.days).created_at_before(DateTime.now)

我按照这里解释的解决方案Rails extending ActiveRecord::Base进行了操作,但是当我执行rails控制台并尝试调用方法时,出现了undefined method错误。

以下是我的代码:

# config/initializers/active_record_date_extension.rb
require "active_record_date_extension"

# lib/active_record_date_extension.rb
module ActiveRecordDateExtension
  extend ActiveSupport::Concern

  included do |base|
    base.columns_hash.each do |column_name,column|
      if ["datetime","date"].include? column.type
        base.define_method("#{column_name}_after") do |date|
          where("#{column_name} > ?", date)
        end
        base.define_method("#{column_name}_before") do |date|
          where("#{column_name} < ?", date)
        end
      end
    end
  end
end
Rails.application.eager_load!

ActiveRecord::Base.descendants.each do |model|
  model.send(:include, ActiveRecordDateExtension)
end

我做错了什么?


你介意贴出堆栈跟踪吗?以及 ActiveRecordDateExtension.instance_methods 的结果? - Jeremy Rodi
@JeremyRodi Rails控制台正常运行。当我尝试调用例如User.created_at_before(DateTime.now)时,会出现“未定义方法”错误。NoMethodError: undefined method 'created_at_before' for #Class:0x007fb500970b00....这是instance_methodsmethods的输出。但请注意,我正在尝试定义类方法。ActiveRecordDateExtension.instance_methods => [] ActiveRecordDateExtension.methods(false) => [] - Simon Soriano
2个回答

4

使用Rails 4.1.9和Ruby 2.2.1时,我注意到上面的代码存在一些问题。

  1. 您正在将column.type与字符串进行比较,而Rails为该属性返回符号。
  2. base.define_method试图调用私有方法,您可以通过send来解决这个问题。

以下是修改后的代码

module ActiveRecordDateExtension
  extend ActiveSupport::Concern

  included do |base|
    base.columns_hash.each do |column_name,column|      
      if [:datetime, :date].include? column.type              
        base.class.send(:define_method, "#{column_name}_after") do |date|
          where("#{column_name} > ?", date)
        end
        base.class.send(:define_method, "#{column_name}_before") do |date|
          where("#{column_name} < ?", date)
        end
      end
    end
  end
end

为什么需要使用base.class而不是只用base? 使用base.class会有一个问题:每个类现在都有这些方法。有没有一种方法可以避免这种情况,只为ActiveRecord::Base的子类定义这些方法? - Simon Soriano
我相信你想要的是 User.created_at_after,对吗?这就是为什么 base.class 必须是你定义该方法的地方。如果你只是在 base 上定义它,那么它就会成为一个实例方法而不是类方法。 - yez

2
感谢之前的回答,我意识到了问题的一部分。以下是所有问题和我在研究后得出的解决方案:
  1. column.type 是一个符号,而我却将其与字符串进行比较。
  2. base.define_method 是一个私有方法。
  3. 我必须在 singleton_class 中定义方法,而不是在 base 类或 class 中定义。
  4. Rails.application.eager_load! 将会导致即使不需要也会急切加载。这并没有影响功能,但首先“扩展”的急切加载不应该是它的职责,在第二个方面它依赖于 Rails,使得“扩展”只能兼容 Rails。
考虑到这些问题,我决定使用 ruby 的 method_missing 功能实现它,并编写了这个 gem (https://github.com/simon0191/date_supercharger)。以下是与此问题相关的部分:
module DateSupercharger
  extend ActiveSupport::Concern

  included do
    def self.method_missing(method_sym, *arguments, &block)
      return super unless descends_from_active_record? 
      matcher = Matcher.new(self,method_sym)
      # Inside matcher
      # method_sym.to_s =~ /^(.+)_(before|after)$/

      if matcher.match?
        method_definer = MethodDefiner.new(self) # self will be klass inside Matcher
        method_definer.define(attribute: matcher.attribute, suffix: matcher.suffix)
        # Inside MethodDefiner
        # new_method = "#{attribute}_#{suffix}"
        # operators = { after: ">", before: "<" }
        # klass.singleton_class.class_eval do
        #   define_method(new_method) do |date|
        #     where("#{attribute} #{operators[suffix]} ?", date)
        #   end
        # end
        send(method_sym, *arguments)
      else
        super
      end
    end

    def self.respond_to?(method_sym, include_private = false)
      return super unless descends_from_active_record?
      if Matcher.new(self,method_sym).match?
        true
      else
        super
      end
    end
  end
end
ActiveRecord::Base.send :include, DateSupercharger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接