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

后端 未结 3 626
终归单人心
终归单人心 2020-12-30 11:12

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<

相关标签:
3条回答
  • 2020-12-30 11:30

    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
    
    0 讨论(0)
  • 2020-12-30 11:42

    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
    
    0 讨论(0)
  • 2020-12-30 11:50

    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.

    0 讨论(0)
提交回复
热议问题