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):
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:
There are two important things to note about how and when these callbacks are run:
First, callbacks like
after_remove will run before and after the
delete methods. In other words, when we call
delete method will invoke the
after_remove callbacks by default. Similarly, the
destroy method will destroy a collection of records and remove them from an association while calling the
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, 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
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
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
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
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_belongs_to_many association classes, while the
CollectionAssociation class itself inherits from the
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
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
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
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
destroy methods! This is exactly how we can confirm what we learned earlier about our
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.
- 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 (
- Need to brush up on
ActiveRecordassociations? Head over to the module documentation for associations.
- Want to know exactly what’s going on inside of the
deletemethods? Dive into the source code! Check out the delete and destroy method signatures.