Words and Code

One writer’s journey from words to code.

Functions to Call Upon: ActiveRecord Association Callbacks

#technicaltuesdays, rails, ruby


No developer knows everything — even if they do know a whole lot more than you do. I was reminded of this fact recently while pairing with a more experienced programmer on a large Ember-Rails project that’s now coming to a close. We were right in the middle of adding some methods to a Rails model when we realized that we actually needed to change something about how one of our model’s ActiveRecord associations worked.

What we wanted to do was rely upon a callback function executing every time an association was created or destroyed. But, we hit a little bit of a wall, since we quickly realized that this wasn’t as simple as we initially thought. The complication came with the fact that we were dealing with a has_many relationship, which meant that we weren’t dealing with a single object, but rather a collection of objects. The developer I was pairing with explained that he knew that there were some methods that ActiveRecord has to achieve what we wanted to do, but he wasn’t sure how they worked exactly, just that he had seen them before elsewhere.

After doing a little research on what resources ActiveRecord provides when it comes to solving this problem in an elegant way, we eventually decided to go another route and use a different callback function. But in the process of pairing, we learned that there are some methods that exist in ActiveRecord that can come in handy in this situation. If we hadn’t been on a tight deadline for the specific feature that we were building, we probably could have devoted more time to learning about these callbacks. So, I decided to come back to these callback functions and dig a little deeper. Interestingly, there really isn’t that much documentation on ActiveRecord’s association callback methods, and nearly no blog posts. This is uncharted territory, my friends! Are you ready? I hope so.

Hello, it’s me, a callback function


Callback functions are everywhere, particularly when we’re inside the context of most Rails’ models. Our average ActiveRecord callback will hook into the object life cycle of our ActiveRecord instances, which allows us to work with our object at any given point during its lifespan. The most common use case for implementing a callback is executing logic before or after the alteration of an object’s state.

Here’s my personal list of my top callbacks (yes, I have a list):

  1. after_commit
  2. after_update
  3. after_destroy
  4. before_validation
  5. before_save

They pretty much do exactly what you think they would do: they hook into the object at the point in it’s create, update, validate, save, or destroy “state”, and allow you to execute whatever functionality you might need. This is just the tip of the iceberg though: check out the whole list of ActiveRecord callbacks that are free to use.

But let’s get back to association callbacks. What makes them different from that bunch of functions we just listed above? Well, there’s really one big difference in particular: association callbacks are similar to normal callbacks, but rather than hooking into the life cycle of a single object, they are triggered by the life cycle of a collection of objects. Unlike “single object” callbacks however, there are a limited number of a methods available for us to use. Actually, there’s exactly four association callbacks, to get specific:

  1. before_add
  2. after_add
  3. before_remove
  4. after_remove

There are two important things to note about how and when these callbacks are run:

First, callbacks like before_remove and after_remove will run before and after the delete methods. In other words, when we call objects.delete, the delete method will invoke the before_remove and after_remove callbacks by default. Similarly, the destroy method will destroy a collection of records and remove them from an association while calling the before_remove and after_remove functions.

Second, if any of the before_add callbacks throws an exception and cannot create the association with a record in the collection, the object simply won’t be added to the collection. Similarly, if any of the objects passed to the before_remove callback throws an exception, the object won’t be removed from the collection. This is pretty important to keep in mind for two reasons: if we want to make sure that objects are only added or removed from an association collection successfully, and want to throw an error of some sort if for some reason this is unsuccessful, this is a really good thing. But, if we want to be able to assume that an object can always be added or removed from a collection without fail, this is bad thing, because we can’t always be sure that this will happen.

With all these points in mind, there’s only one question left to ask: how do we implement these callbacks, exactly? Time to find out.

Calling Upon Callbacks


Implementing our association callback isn’t too complicated of a task. Since these four association callbacks can only be invoked on a has_many or has_many, through: collection association, our callbacks can be added onto the same line where this association is defined in our model. For example, let’s say that our Order objects can have many Discounts (apparently we’re feeling particularly generous this holiday season, and we’re going to allow many discounts rather than just one!). We can start off with a model that looks like this:

1
2
3
class Order
  has_many :discounts, after_add: :recalculate_total
end

Whenever a discount is added to an order instance, we want to recalculate the total for our order, with the discount applied. We’ve added the callback to our association, and are passing our callback the name of the function (in this case, we’ve called it recalculate_total) we want to be executed every single time a discount is added to our “collection” of discounts on an order.

Now, all we need to do is actually write the recalculate_total method. Since we don’t want this method to be called in any context aside from this association callback, we’ll make it a private method:

1
2
3
4
5
6
7
8
9
10
11
class Order
  has_many :discounts, after_add: :recalculate_total

  private

    def recalculate_total(discount)
      subtotal = items.map(&:amount).inject(:+)

      update!(total: (subtotal - discount))
    end
end

We can really pass as many methods to our callbacks as we want, in the form of symbols in an array. We can also pass a proc directly into this array as well, as explained by the Rails docs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Order
  has_many :discounts, after_add: [:recalculate_total,
  :touch_discount_applied_at]

  private

    def recalculate_total(discount)
      subtotal = items.map(&:amount).inject(:+)

      update!(total: (subtotal - discount))
    end

    def touch_discount_applied_at
      touch(:discount_applied_at)
    end
end

Pretty interesting, right? Who knew that this even existed?! (Spoiler alert: me. I had no clue this was even a thing.) But how does this even happen? Where is this defined, and how does it work? Let’s dig one level deeper and dive into my favorite place: the Rails source code.

All About The Association Builder

I think one of the reasons that association callbacks aren’t as well-known or written about is because they live in an odd place. In fact, they are defined inside of ActiveRecord’s Associations::Builder::CollectionAssociation (wow, what a mouthful!) class, which doesn’t actually have any documentation at all! No wonder this is Rails best-kept secret! This class is actually inherited by the has_many and has_many_belongs_to_many association classes, while the CollectionAssociation class itself inherits from the Association class.

I’m still not completely sure what’s going on behind the scenes here, but from the source code it appears as though the association callbacks (namely before_add, after_add, before_remove, and after_remove to be exact) are all defined as an array of symbols, which is set to a CALLBACKS constant, which is used throughout the class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require 'active_record/associations'

module ActiveRecord::Associations::Builder
  class CollectionAssociation < Association

      CALLBACKS = [:before_add, :after_add,
      :before_remove, :after_remove]

      def self.define_callbacks(model, reflection)
          # Truncated for brevity!

          CALLBACKS.each { |callback_name|
              define_callback(model, callback_name, name, options)
          }
      end
    end
end

So, if this is where our association callbacks are defined, where are they being invoked? I’m glad you asked. It turns out, they’re used most often inside of ActiveRecord’s Associations::CollectionAssociation module, which contains methods that we use all the time on our ActiveRecord objects — perhaps without even realizing it! Our association callbacks are passed as arguments and then implemented by the callback method in CollectionAssociation module.

We can actually see our callbacks in action inside of a private method called remove_records, which is defined within this very same module. Here’s what that method looks like:

1
2
3
4
5
6
7
8
def remove_records(existing_records, records, method)
  records.each { |record| callback(:before_remove, record) }

  delete_records(existing_records, method) if existing_records.any?
  records.each { |record| target.delete(record) }

  records.each { |record| callback(:after_remove, record) }
end

Nice! There’s our after_remove callback, being called on each of our records that’s being passed into it. We might not even realize it, but the remove_records method is called upon by the delete_or_destroy method (another private method in this module), which is invoked by both of ActiveRecord’s commonly-used delete and destroy methods! This is exactly how we can confirm what we learned earlier about our before_remove and after_remove callbacks. It’s not magic — it’s just code that’s hiding from us! Except now we know how to call upon our callbacks, so we can say that we are strangers no more.


tl;dr?

  • Association callbacks are similar to ActiveRecord’s normal callbacks, except that they hook into the life cycle of a collection of objects (has_many), rather than just a single object (has_one).
  • Need to brush up on ActiveRecord associations? Head over to the module documentation for associations.
  • Want to know exactly what’s going on inside of the destroy and delete methods? Dive into the source code! Check out the delete and destroy method signatures.