简体   繁体   English

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

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

I wrote a simple Cacheable module that makes it simple to cache aggregate fields in a parent model. 我编写了一个简单的Cacheable模块,可以很容易地在父模型中缓存聚合字段。 The module requires that the parent object implement the cacheable method and a calc_ method for each field that requires caching at the parent level. 该模块要求父对象为需要在父级别进行缓存的每个字段实现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

I would like to add callbacks to the ActiveRecord model into which this module is included. 我想将回调添加到包含此模块的ActiveRecord模型中。 This method would require that the model implement a hash of parent models and field names that require caching. 此方法将要求模型实现父模型的哈希值和需要缓存的字段名称。

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

This approach works great if I manually add both callbacks using such as: 如果我使用如下手动添加两个回调,这种方法很有效:

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

But, I'm receiving the following error when I try to use the cachebacks method to add these to callbacks. 但是,当我尝试使用cachebacks方法将这些添加到回调时,我收到以下错误。

cachebacks(Quote, "*quotes.all")

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

How do I add these callbacks to the class dynamically? 如何动态地将这些回调添加到类中?

This looks like a good case for ActiveSupport::Concern . 对于ActiveSupport::Concern这看起来很好。 You can tweak your cachebacks method slightly to add it as a class method on the including class: 您可以稍微调整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

To use it: 要使用它:

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

The block you pass to cachebacks will be executed in the context of the class that's calling it. 传递给cachebacks的块将在调用它的类的上下文中执行。 In this example, { all } is equivalent to calling Example.all and passing the results into your cache! 在此示例中, { all }等效于调用Example.all并将结果传递到cache! method. 方法。


To answer your question in the comments, Concern encapsulates a common pattern and establishes a convention in Rails. 为了在评论中回答您的问题, Concern封装了一个通用模式并在Rails中建立了一个约定。 The syntax is slightly more elegant: 语法略显优雅:

included do
  # behaviors
end

# instead of

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

It also takes advantage of another convention to automatically and correctly include class and instance methods. 它还利用另一个约定来自动且正确地包含类和实例方法。 If you namespace those methods in modules named ClassMethods and InstanceMethods (although as you've seen, InstanceMethods is optional), then you're done. 如果在名为ClassMethodsInstanceMethods模块中命名这些方法(尽管如您所见, InstanceMethods是可选的),那么您就完成了。

Last of all, it handles module dependencies. 最后,它处理模块依赖性。 The documentation gives a good example of this, but in essence, it prevents the including class from having to explicitly include dependent modules in addition to the module it's actually interested in. 文档提供了一个很好的例子,但实质上,它阻止了包含类除了它实际感兴趣的模块之外还必须显式包含依赖模块。

Thanks to Brandon for the answer that helped me write the solution. 感谢布兰登给出了帮助我编写解决方案的答案。

Add the following to your model. 将以下内容添加到模型中。 You can cacheback multiple parent relationships per model. 您可以为每个模型cacheback多个父关系。 You can also specify different attribute names for the parent and child tables by passing in a hash instead of a string for a particular field. 您还可以通过传入哈希而不是特定字段的字符串来为父表和子表指定不同的属性名称。

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

This module extends ActiveSupport::Concern and adds the callbacks and performs the cacheing. 该模块扩展了ActiveSupport :: Concern并添加了回调并执行缓存。 Your parent classes will need to implement calc_field methods to do the caching work. 您的父类需要实现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

As I said in the comment, I might not be right if I didn't understand your question. 正如我在评论中所说,如果我不理解你的问题,我可能不对。 Would this work for you? 这对你有用吗?

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