A Step by Step guide to Active Record Relationship Aliasing
The entire third week of the Flatiron software engineering course is dedicated to building a command line interface which uses a domain model that we implement in ActiveRecord. With my similarly overambitious partner Jessica Betts, we produced this hunk of a domain for our Game of Thrones CLI:
I love UML, and as soon as we were introduced to domain modeling and relational databases at flatiron, I went crazy for it (as you can probably tell), but what I didn’t know going into this, is that relationships can get more complex than just has_many
, belongs_to
, and has_many, through:
Most of this model that we designed consists of just those relationships, which took all of 5 minutes to implement with Active Record. But at this point, I was only familiar with using one argument in a has_many
statement (or sometimes two with the through:
keyword), but that pattern soon proved to not be enough for the complexity of our model.
If you’ve used ruby Active Record for any amount of time, you know that having consistent names throughout your project is very important.
When I was first starting with AR, I learned very quickly that your class names, table names, foreign key names, and relationship symbols all had to be perfectly spelled and formatted by Active Record standards (class names are PascalCased singular, table names are snake_cased plural etc.) for the relationships to work. On a deep level this made sense; how else is AR going to write code that correctly accesses all of these things? But until the system stopped working, I never appreciated how much those symbols that you pass to the relationship macros (e.g. has_many :dogs
and belongs_to :owner
) are responsible for.
This single symbol argument pattern stopped working when we tried to implement the Murder model (This example is quite morbid... but Game of Thrones wouldn’t be Game of Thrones without murder). Let’s look at this model more specifically:
What you're looking at here is a self join table, a slightly more complex relationship that breaks the typical relationship calls.
This relationship is an example of a self join table; self join tables function exactly like join tables would between different classes, but for one class. That is to say that instead of joining two objects of two different classes, it joins two objects of the same class. At first this seems very intuitive, but working through it, we see that the most basic Active Record conventions fail to describe this domain.
But fear not, Active Record gives us more tools than even the most seasoned developers could ever use, and one of the most simple ones: aliasing will solve all of our self join problems!
Let's do it:
Let's start by thinking about this relationship in plain english: For a Murder to exist, it has to have a murderer and a victim, both of which are Character instances. This makes total sense intuitively, and is easily implemented in the Murder class:
But when we test this code (by manually creating a Murder object) we get an error!
pry(main)> Murder.first.victim
NameError: uninitialized constant Murder::Victim
from /Users/nissenadam/.rvm/gems/ruby-2.3.3/gems/activerecord-5.2.2/lib/active_record/inheritance.rb:196:in `compute_type'
The relationship is looking for a Murderer class to match its :murderer method, but the murderer is just a Character object. The solution to this problem is that the ActiveRecord association macros can take more arguments to alias the various things that they are responsible for matching (The foreign key, class name, and table name)
NOTE: In ActiveRecord 4 there is another method for this using the alias_attribute
method, but in my opinion it makes code more confusing. ¯\_(ツ)_/¯
Going back to the Murder class with this knew knowledge, we were able to tell the belongs_to
method that both the murderer and the victim were actually Character objects using the :class_name
keyword argument:
The :class_name
argument updates the class name that it is looking for (duh) so that it knows that the murderer and the victim are both Character objects. This also makes it compatible with the database, because it looks for the foreign keys in the database that match the class name, which we’ve just set to ”Character”
Moving on to the Character model, we first need to think about its relationship to the Murder model: it actually has 2! We want our characters to know about who they’ve murdered, and who murdered them. Thinking of Murder objects, we can think of these different relationships as deaths and kills respectively (I told you it gets morbid). Lastly we need to remember that both of these methods actually point to a Murder object, so we have to alias them, which would look something like this:
But when we try to test this we get this error:
pry(main)> Character.first.death
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: murders.character_id: SELECT "murders".* FROM "murders" WHERE "murders"."character_id" = ? LIMIT ?
from /Users/nissenadam/.rvm/gems/ruby-2.3.3/gems/sqlite3-1.3.13/lib/sqlite3/database.rb:91:in `initialize'
Caused by SQLite3::SQLException: no such column: murders.character_id
from /Users/nissenadam/.rvm/gems/ruby-2.3.3/gems/sqlite3-1.3.13/lib/sqlite3/database.rb:91:in `initialize'
Here the has_many
/has_one
macros are looking for the foreign key “character_id” in the murders table, which it gets from the name of the class, but as we know, the foreign keys in the murders table are murderer_id and victim_id. Lucky for us, we can alias the foreign keys in both has_
statements to get this working too!
Now a Murder is able to access its murderer and victim, and a Character is able to access all of its kills and it’s death.
All that is left is for characters to know their murderer and victims - which they can get through their death and kills respectively. These relationships translate pretty seamlessly from plain english into ActiveRecord code, so let’s write them:
And with that, our code is working! characters can know their victims and murderer, their kills and their death, and the Murder model functions successfully as a join between the two and can be expanded on.
That's cool I guess..
This was just an intro to Active Record aliasing and modeling more complex relationships.
While self join tables and foreign_key aliasing are super rad, they're really only the tip of the iceberg! Looking through the source code for these relationship macros here, we can see that the aliasing patterns we just used are just some of a host of different options that they can take. Also if SQL doesn't give you a massive headache, using the .to_sql
command after relationship methods can show exactly how Active Record implements these relationships (e.g. Character.first.victims.to_sql
)
A quick example of another very common option (which is also for aliasing) is the :source
option in the has_
macros, which can modify the foreign key it is looking for (the same way the :foreign_key
option does for belongs_to
.
Even if you can't imagine any domains that would need these options (aliasing or otherwise), looking through these options and trying to figure our what they do has deepened my general understanding of active record by a huge amount. Active Record is a crazy powerful tool that will support all of your rails applications, so lifting up the hood and becoming an Active Record master will take your back-end skills to a new level.
Top comments (0)