ActiveRecord Internals : You are not ready for this

Introduction to sorcery

I like this quote: Magic is the technology we still need to understand. Rails is an excellent framework; it hides complexity from developers, but because of that, many people qualify it as magical.

This article will demystify Rails by looking deeper into the Rails active record association internals. In the meantime, we will also try to understand pretty cool programming concepts.

I hope you are ready ( you are not ), it's going to be fun, let's go.

Oh yes, and if you like to learn or read about Rails, Ruby, databases, and a lot of tech-related stuff :

Keep in Touch

On Twitter/X : [@yet_anotherDev] (https://twitter.com/yet_anotherDev)

On Linkedin : Lucas Barret

Did you say association?

Let's begin with defining active record associations and why they are helpful.

If you are a Rails developer, you should know, and I am sure you know it, this fantastic website : Rails Guides.

Going to this link and you can read that : association is a connection between two active record models, and that's all. But Why do they exist? Because it makes everyday operations straightforward and more accessible.

Nice, keep reading and we see that Associations are implemented using macro-style programming ?

Macro style Ruby Fu

A lot of definition in this article but yet this is useful to be sure we all understand the same thing. Now going to Wikipedia we can see that : a macro is a rule or a pattern that specifies how a specific input should be mapped to a replacement output.

This makes everyday tasks or definitions available for programmers as a single statement. ( Like your associations statements ).

This kind of programming style enables define our association in a declarative way. If you define an Athlete class that has_many Medal(s), you will write it as it :

class Medal < ActiveRecord::Base
    belongs_to :athlete
end

class Athlete < ActiveRecord::Base
    has_many :medals
end

You declare that your athlete has_many medals and that medal belongs_to athlete.

In ruby, macros and this kind of declarative style of programming are possible thanks to the class-level method but also thanks to how ruby classes work.

Go back to class

Ruby Classes are no unique code; as with everything else in Ruby, they are executable.

If I define a Ruby class called: Athlete, with a workout class method. I could call it inside the class, and when my ruby interpreter reads my class code, it will execute my workout method.

#./athlete.rb
class Athlete 
  def self.workout
    p '1 push up'
   end

   workout
end
> ruby athlete.rb
> 1 push up

As you can see, your class definition code is executed!

It is precisely what is happening with your has_many declaration. It is a method call with an argument which is the name of your associate model in a simple case we illustrated earlier.

Dancing in the dark

It is time to dive into rails code; let's see what we can find. As we want to find out how has_many associations work, the first step is to look for a definition of a method called has_many.

If you look in the ActiveRecord code, you will find :

#rails/activerecord/associations.rb
def has_many(name, scope = nil, **options, &extension)
  reflection = Builder::HasMany.build(self, name, scope, options, &extension)
  Reflection.add_reflection self, name, reflection
end

But this is weird, and you could ask why this is not defined as a class method.

Looking around, this is a class method defined in the module ClassMethods, with another pattern we won't cover here.

Now in the internal of this function, little is done. We build a reflection and add it to the Reflection class with the association name.

If we go to the rails/activerecord/associations/builder/has_many.rb file, what we see is not helpful for what we want to understand at first sight.

We see the options available for our association and the dependent options. But there has yet to be a build class methods function.

module ActiveRecord::Associations::Builder # :nodoc:
  class HasMany < CollectionAssociation # :nodoc:
    def self.macro
      :has_many
    end

    def self.valid_options(options)
...
    end

    def self.valid_dependent_options
...
    end
        private_class_method :macro, :valid_options, :valid_dependent_options
  end
end

Nevertheless, this class inherits from CollectionAssociation let's see what we got in rails/activerecord/associations/builder/collection_association.rb; there are many cool things here, but still no build methods.

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

But as before, this class inherits from another class, Association. Let's see this one.

And this is what we are looking for. We have the build methods, which all the associations share.

And in this method, the famous reflection is built and returned to the has_many class methods.

def self.build(model, name, scope, options, &block)
  if model.dangerous_attribute_method?(name)
    raise ArgumentError, "You tried to define an association named #{name} on the model #{model.name}, but " \
                         "This will conflict with a method #{name} already defined by Active Record. " \
                         "Please choose a different association name."
  end

  reflection = create_reflection(model, name, scope, options, &block)
  define_accessors model, reflection
  define_callbacks model, reflection
  define_validations model, reflection
  define_change_tracking_methods model, reflection
  reflection
end

But why is reflection needed? What is a reflection?

This will be the subject of my next article...

Conclusion

We have begin our journey in the dark magic Rails. If you want to feel powerful, wait for the second one next week.

If you liked this article and would like to know more or hear from it differently: check this keynote from Daniel Colson.

I dove into the code and began this article before seeing the keynote. Then I was like, should I not publish it? Eventually, I got more knowledge from this conference and tried to improve the article the best I could. I hope this article will help my fellow rubyists.

See you for the second part next week.