DEV Community

Lucas Barret
Lucas Barret

Posted on

ActiveRecord Internals: You are still not ready

This is part 2 of a series of articles I have begun recently. I try to understand better Rails ActiveRecord and how this is designed internally.

In the last article, we discussed reflection but did not dive into it.

Now we still have some black spells to explore

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

On Linkedin : Lucas Barret

Mirror, who is the prettier?

To remind you a bit of what we did in the last article. We have found the has_many method where we build reflection and add things to this reflection.

#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
Enter fullscreen mode Exit fullscreen mode

But we did not answer the question of why we need this reflection. And what are reflections in fact? Let's risk ourselves to some mysterious coding concepts again.

Reflection is an old concept, it can be described as the ability of some code to modify its code and structure at runtime. And this is key in metaprogramming. Without this concept, many cool things we like in Ruby and Rails would not exist.

We need to retrieve dynamically the instance of the model associated with the current model. We need to define accessors for the associated model.

If we get this example which is the same as our precedent article :

class Medal < ActiveRecord::Base
    belongs_to :athlete
end

class Athlete < ActiveRecord::Base
    has_many :medals
end
Enter fullscreen mode Exit fullscreen mode

When we define these classes and associations. It is consistent to be able to access our medals from our athletes with something like this : athlete.medals to get all the medals of a specific athlete.

This relies on two concepts one that we are discussing, Reflective programming, and another, which is Mixin, which we are going to study a bit later.

Reflexive programming

So even if you have declared your association in your class. You will be able to know what this association is at runtime and not before. This is compromising the fact to be able to do this :athlete.medals. How can you define code at runtime?

For that, you'll need reflection; defining accessors at runtime after your ruby interpreter has effectively associated your two instances will not be an issue anymore.

How you can do that? You define your active record and the name of the association. And thanks to that, if you respect Rails convention, we can retrieve the class of your active record and the table name. You can then define the readers and the writers for it at runtime.

Let's see how :

def self.define_accessors(model, reflection)
      mixin = model.generated_association_methods
      name = reflection.name
      define_readers(mixin, name)
      define_writers(mixin, name)
    end

  def self.define_readers(mixin, name)
      super

      mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{name.to_s.singularize}_ids
          association(:#{name}).ids_reader
        end
      CODE
    end

    def self.define_writers(mixin, name)
      super

      mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{name.to_s.singularize}_ids=(ids)
          association(:#{name}).ids_writer(ids)
        end
      CODE
    end
Enter fullscreen mode Exit fullscreen mode

This code does exactly what we said for the ids but the idea is the same for the object instance accessors. You define dynamically thanks to the name of the association and the accessors to it.

But then another pattern which is mysterious and obscure makes its appearance: mixin. There is also a bit of metaprogramming with class_eval that enables to define methods at runtime.

Mixin' all together

The mixin pattern is needed for one reason. Ruby does not support multiple inheritance. And then, if you already have inherited from a class, you are doomed.

But you can use a cool trick with the module and include keyword. This is the way of ruby to deal with multiple inheritance. There is this cool article by GeekForGeeks that you can check if you want.

But to make it clearer :

##mixin_test.rb
module Module1
    def module_method1
        p 'module 1'
    end
end

module Module2
    include Module1

    def module_method2
        p 'module 2'
    end

    class ClassMixin
        def class_method1
            p 'class mixin'
        end
    end
end

cla = Class1.new 
cla.class_method1
cla.module_method2
cla.module_method1
Enter fullscreen mode Exit fullscreen mode
> ruby mixin_test.rb
> class mixin
> module 2
> module 1
Enter fullscreen mode Exit fullscreen mode

All instance methods are available to the ruby class thanks to the mixin and the include keyword. And that's all folks!

Now we have this, we can include our mixin in our Reflection, and once we will access our field, the method will be defined in the module, and we will be able to call them on the instance.

Conclusion

Eventually, all this magic we experiment with Rails is, in fact, a good ol' trick and not sorcery. We need a lot of techniques, though, to make our beloved framework work, from reflection to mixin and macros.

That's it with completing our first journey in the sorcery of rails. I hope you understand a bit better how active record association works.

There is a lot to discover and understand; we have barely scratched the surface. But it feels good to understand a bit more about the technologies we use each day :).

Top comments (0)