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):
after_commit
after_update
after_destroy
before_validation
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:
before_add
after_add
before_remove
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 Discount
s (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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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
anddelete
methods? Dive into the source code! Check out the delete and destroy method signatures.