Creating ActiveRecord Associations

I was working on something recently that made me question how ActiveRecord creates its associations under the hood, so I peeked into the source code to figure out what was actually happening.

If you want to follow along, you can do:

cd ~/my_rails_project
cd `bundle show activerecord`
vim . # or whatever editor you use

I’m going to focus on the has_one association, but all the associations are similar with slight implementation differences.

Here’s our example class with the has_one association:

So let’s see how a Job gets associated to a Person when we run:

Person.create!(name: 'Bob Loblaw', job: 'Writer'))

The has_one function is declared in lib/active_record/associations.rb with the following definition (note that the line numbers I’ll reference throughout this post reflect the line numbers displayed in the gists, which are not reflective of the real line numbers in the source code):

Along with the has_one definition, you’ll be able to find all the other association definitions in this file as well.

As shown in the definition, this calls Let’s take a look at that class:

You’ll notice that build isn’t defined in this class, so let’s take a look at its parent class SingularAssociation:

Again, you’ll notice that build isn’t defined here either. That’s because it’s defined in its parent class Association:

The relevant method is define_callbacks. The model parameter is our Person. If you’re unfamiliar with reflections, you can think of them as association descriptions. So in this case, reflection contains information about our Person having_one Job. The if block on L21 is for destroying associations, so we’ll skip that. The important stuff is happening on L26 where the extensions are calling build on our params. What are these extensions and where are they coming from? The extension that we care about is AssociationBuilderExtension which is located in lib/active_record/autosave_association.rb:

You’ll notice that we’re calling add_autosave_association_callbacks on the model in the build method. If you open up rails console, you’ll see that this method exists as a private instance method on the singleton class:

Person.singleton_class.private_instance_methods.find do |m|
m.match 'add_autosave'
=> :add_autosave_association_callbacks

Now let’s jump down to L22 and examine that function. The first thing we do is create a function name based on the reflection parameter. In our case, this will be autosave_associated_records_for_job.

Since we have a has_one association, the program will enter the elsif block on L32. On L33 we define a method named autosave_associated_records_for_job and pass in another function, save_has_one_association as its definition. You can verify that autosave_associated_records_for_job exists on our model by doing:

Person.instance_methods.find do |m|
m.match 'autosave_associated_records_for_job'

That function is where all the magic is happening, and we’ll explore it in a moment. The last two lines of code tell our model to run the function we just defined on after_create and after_update. You can see that these callbacks are defined on Person:

Person._create_callbacks.to_a.find do |c|
c.filter.match 'autosave_associated_records_for_job`
=> :autosave_associated_records_for_jobs
Person._update_callbacks.to_a.find do |c|
c.filter.match 'autosave_associated_records_for_job`
=> :autosave_associated_records_for_jobs

Now let’s take a look at the save_has_one_association function:

The first few lines of code retrieve our Job record and ensure that it exists before we proceed. On L9 we retrieve the autosave option, which allows us to specify the type of behavior that should occur between associations during mutating events. In our case, autosave is nil because it wasn’t defined as an option in our has_one :job declaration. As a result, the elsif condition will trigger because nil != false. We’ll then proceed to assign the foreign key on Job and save the record.

To make a long story short, whenever you define an association on a model like has_one, has_many, etc ActiveRecord will create an after_create callback that also creates the associated model.