如何使用Ruby元编程将回调添加到Rails模型?
我编写了一个简单的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 #
如何动态地将这些回调添加到类中?
对于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
它还利用另一种约定来自动且正确地包含类和实例方法。 如果在名为ClassMethods
和InstanceMethods
模块中命名这些方法(尽管如您所见, 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