[英]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. 如果在名为ClassMethods
和InstanceMethods
模块中命名这些方法(尽管如您所见, 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.