繁体   English   中英

如何使用Ruby元编程将回调添加到Rails模型?

[英]How do I use Ruby metaprogramming to add callbacks to a Rails model?

我编写了一个简单的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>

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

对于ActiveSupport::Concern这看起来很好。 您可以稍微调整cachebacks方法,将其作为类方法添加到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! 方法。


为了在评论中回答您的问题, Concern封装了一个通用模式并在Rails中建立了一个约定。 语法略显优雅:

included do
  # behaviors
end

# instead of

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

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

最后,它处理模块依赖性。 文档提供了一个很好的例子,但实质上,它阻止了包含类除了它实际感兴趣的模块之外还必须显式包含依赖模块。

感谢布兰登给出了帮助我编写解决方案的答案。

将以下内容添加到模型中。 您可以为每个模型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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM