I wrote a simple Cacheable module that makes it simple to cache aggregate fields in a parent model. The module requires that the parent object implement the cacheable<
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
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. 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. Your parent classes will need to implement calc_field
methods to do the caching work.
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
This looks like a good case for ActiveSupport::Concern. You can tweak your cachebacks
method slightly to add it as a class method on the including class:
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. In this example, { all }
is equivalent to calling Example.all
and passing the results into your cache!
method.
To answer your question in the comments, Concern
encapsulates a common pattern and establishes a convention in 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.
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.