Home Blog Talks

Managing Model Callbacks with Subscriber

2017-03-27

To avoid making the Model file too long, move the callbacks into a Subscriber.

Scenario and Problem

Solution

Simple Example

When updating the product price, it is necessary to update the price of related orders.

class Product < ApplicationRecord
  has_many :orders

  after_update do
    if price_changed?
      orders.each do |order|
        order.update! price: price
      end
    end
  end
end

After Using Subscriber

# ./app/subscribers/order_subscriber.rb
subscribe_model :product, :after_update do
  if price_changed?
    orders.each do |order|
      order.update! price: price
    end
  end
end

Using Conventions

Benefits

Core Code

# ./config/initializers/subscribe_model.rb
def subscribe_model(model_name, event_name, &block)
  ActiveSupport::Notifications.subscribe("active_record.#{model_name}.#{event_name}") do |_name, _started, _finished, _unique_id, data|
    data[:model].instance_eval(&block)
  end
end

class ActiveRecord::Base
  class_attribute :skip_model_subscribers
  self.skip_model_subscribers = false
end

%i(after_create after_update after_destroy after_save after_commit after_create_commit after_update_commit).each do |name|
  ActiveRecord::Base.public_send(name) do
    unless skip_model_subscribers
      readonly! unless readonly?
      ActiveSupport::Notifications.instrument "active_record.#{self.class.model_name.singular}.#{name}", model: self
      ActiveSupport::Notifications.instrument "active_record.#{self.class.base_class.model_name.singular}.#{name}", model: self if self.class.base_class != self.class
      public_send(:instance_variable_set, :@readonly, false)
    end
  end
end

Rails.application.config.after_initialize do
  Dir[Rails.root.join('app/subscribers/*.rb')].each { |f| require f }
end

FAQ

Q: Why not use a concern?

A: Within the team, it is agreed that only when two or more models share common code should it be moved to a concern.

Q: Why not create a service layer?

A: The service layer is not intuitive enough, and for small teams, the maintenance cost is higher than that of a subscriber.

Back to all posts