【问题标题】:How do I use Ruby metaprogramming to add callbacks to a Rails model?如何使用 Ruby 元编程向 Rails 模型添加回调?
【发布时间】:2012-08-18 12:46:41
【问题描述】:

我编写了一个简单的 Cacheable 模块,它使得在父模型中缓存聚合字段变得简单。该模块要求父对象实现cacheable方法,并为每个需要在父级别缓存的字段实现calc_方法。

module Cacheable
  def cache!(fields, *objects)
    objects.each do |object|
      if object.cacheable?
        calc(fields, objects)
        save!(objects)
      end
    end
  end

  def calc(fields, objects)
    fields.each { |field| objects.each(&:"calc_#{field}") }
  end

  def save!(objects)
    objects.each(&:save!)
  end
end

我想向包含此模块的 ActiveRecord 模型添加回调。此方法需要模型实现需要缓存的父模型和字段名称的哈希。

def cachebacks(klass, parents)
  [:after_save, :after_destroy].each do |callback|
    self.send(callback, proc { cache!(CACHEABLE[klass], self.send(parents)) })
  end
end

如果我使用以下方法手动添加两个回调,这种方法效果很好:

after_save proc { cache!(CACHEABLE[Quote], *quotes.all) }
after_destroy proc { cache!(CACHEABLE[Quote], *quotes.all) }

但是,当我尝试使用 cachebacks 方法将这些添加到回调时,我收到以下错误。

cachebacks(Quote, "*quotes.all")

NoMethodError: undefined method `cachebacks' for #<Class:0x007fe7be3f2ae8>

如何将这些回调动态添加到类中?

【问题讨论】:

  • 抱歉,我无法理解最后一部分。 Quote 是相关型号吗?您能否发布一下您的班级现在的情况?
  • 我根据您的提示得出的答案应该可以解释问题。我也对 ActiveSupport::Concern 方法感兴趣。

标签: ruby-on-rails ruby metaprogramming


【解决方案1】:

正如我在评论中所说,如果我不理解您的问题,我可能不对。这对你有用吗?

module Cacheable
  def self.included(base)
    base.class_eval do
      def self.cachebacks(klass, parents)
        [:after_save, :after_destroy].each do |callback|
          self.send(callback, proc { cache!(CACHEABLE[klass], self.send(parents)) })
        end
      end
    end
  end

  def cache!(fields, *objects)
    objects.each do |object|
      if object.cacheable?
        calc(fields, objects)
        save!(objects)
      end
    end
  end

  def calc(fields, objects)
    fields.each { |field| objects.each(&:"calc_#{field}") }
  end

  def save!(objects)
    objects.each(&:save!)
  end
end

【讨论】:

  • 虽然这不是完全解决方案,但您的回答帮助我弄清楚了。我将在下面添加完整的解决方案作为单独的答案。如果您调整您的,我会将其标记为已接受。非常感谢。
【解决方案2】:

这对于ActiveSupport::Concern 来说似乎是一个很好的案例。您可以稍微调整您的 cachebacks 方法,将其添加为包含类的类方法:

module Cacheable
  extend ActiveSupport::Concern

  module ClassMethods
    def cachebacks(&block)
      klass = self
      [:after_save, :after_destroy].each do |callback|
        self.send(callback, proc { cache!(CACHEABLE[klass], *klass.instance_eval(&block)) })
      end
    end
  end

  def cache!(fields, *objects)
    # ...
  end

  # ...
end

使用它:

class Example < ActiveRecord::Base
  include Cacheable
  cachebacks { all }
end

您传递给cachebacks 的块将在调用它的类的上下文中执行。在此示例中,{ all } 等效于调用 Example.all 并将结果传递给您的 cache! 方法。


为了回答您在 cmets 中的问题,Concern 封装了一个通用模式并在 Rails 中建立了一个约定。语法稍微优雅一些​​:

included do
  # behaviors
end

# instead of

def self.included(base)
  base.class_eval do
    # behaviors
  end
end

它还利用另一个约定来自动正确地包含类和实例方法。如果您在名为 ClassMethodsInstanceMethods 的模块中命名这些方法(尽管如您所见,InstanceMethods 是可选的),那么您就完成了。

最后,它处理模块依赖关系。文档给出了一个很好的例子,但从本质上讲,它阻止了包含类必须显式地包含除了它实际感兴趣的模块之外的依赖模块。

【讨论】:

  • 这是一个不错的解决方案。我以前没有使用过 ActiveSupport::Concerns。与我在下面发布的方法(可缓存模块)相比,主要优点是什么?
  • 我已经更新了我的答案,提供了有关 Concern 的更多详细信息。
【解决方案3】:

感谢 Brandon 的回答帮助我编写了解决方案。

将以下内容添加到您的模型中。您可以为每个模型cacheback 多个父关系。您还可以通过为特定字段传递哈希而不是字符串来为父表和子表指定不同的属性名称。

include Cacheable
cacheback(parent: :quotes, fields: %w(weight pallet_spots value equipment_type_id))

此模块扩展 ActiveSupport::Concern 并添加回调并执行缓存。您的父类需要实现 calc_field 方法来完成缓存工作。

module Cacheable
  extend ActiveSupport::Concern

  module ClassMethods
    def cacheback(options)
      fields = Cacheable.normalize_fields(options[:fields])
      [:after_save, :after_destroy].each do |callback|
        self.send(callback, proc { cache!(fields, self.send(options[:parent])) })
      end
    end
  end

  def cache!(fields, objects)
    objects = objects.respond_to?(:to_a) ? objects.to_a : [objects]
    objects.each do |object|
      if object.cacheable?
        calc(fields, objects)
        save!(objects)
      end
    end
  end

  def calc(fields, objects)
    fields.each do |parent_field, child_field|
      objects.each(&:"calc_#{parent_field}") if self.send("#{child_field}_changed?".to_sym)
    end
  end

  def save!(objects)
    objects.each { |object| object.save! if object.changed? }
  end

  def self.normalize_fields(fields)
    Hash[fields.collect { |f| f.is_a?(Hash) ? f.to_a : [f, f] }]
  end

end

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-04-08
    • 2012-01-05
    • 2011-05-19
    • 2013-04-21
    • 1970-01-01
    • 2017-01-19
    • 1970-01-01
    • 2011-12-11
    相关资源
    最近更新 更多