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] (https://twitter.com/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
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
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
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
> ruby mixin_test.rb
> class mixin
> module 2
> module 1
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 :).