My Experience with ActiveRecord Macros for Self-Referential Associations (Family Tree)

Zachary Dagnall
Nerd For Tech
Published in
5 min readFeb 6, 2021

--

Using Ruby and ActiveRecord, I attempted to create a domain model for a Family Tree. During the process I learned several new things, including but not limited to:

  • “Filial” is a nifty lil’ word for a parent-child type of relationship.
  • There is no nifty lil’ word for the gender-neutral version of aunts and uncles! (Like mother and father → parents; but aunt and uncle → ???) I ended up using the word “auntcles”.
  • The same problem persists with niece and nephew, hence another new zak-created word was born: “niephcews”. Gesundheit. Yeah, I don’t like looking at this word either, but blame the English language, not me.
  • How to spell ‘Gesundheit.’

Okay, so enough about the English and German languages, and back to another language: Ruby. One thing that was challenging about modeling this, which I hadn’t run into before, was the idea of self-referentials. If you’re not familiar with the term, it’s a fancy shmancy OOP (Object-Oriented Programming) word for when objects have relationships with the same kinds of objects. So, if you’re curious about a Truck and its many Wheels, that relationship is not self-referential — Trucks and Wheels are different things. But any sort of relationship between one Person and another Person (brother/sister, classmate/classmate, boss/employee, coworker/coworker, student/teacher, etc) is self-referential, since both of the objects in question are of the Person type.

You could avoid this whole self-referential idea by making, say, a Boss class and an Employee class, instead of using Person and Person. And that may work well in many situations… until you have a boss who also works for someone else, and your employee who is now a manager, and now bosses have bosses and employees have employees and employees are bosses… and you start to see the problem. Your Teacher and Student classes work perfectly fine until your Teacher decides to go back for a second degree and is now a Student on the side while still being a Teacher. So, when we need to represent these more robust situations, as well as multiple simultaneous relationships, we need to let People be People, instead of only being defined by one of their relationships.

So in my Family Tree, there are various kinds of relationships (parent/child, siblings, marriages, etc), and a parent of one Person is also a child of another, and your sibling is also somebody’s husband, etc. But each one of these relationships is between two People objects: hence self-referentials.

Okay, so we get the idea of self-referentials now. But why should self-referential scenarios be any trickier to program than non-self-referential ones? Well the difficulty I had was in setting up the macros in ActiveRecord to appropriately model the relationships. If we had Teacher and Student classes, we could do something like:

class Teacher < ActiveRecord::Base
has_many :grades
has_many :students, through: :grades
end
class Student < ActiveRecord::Base
has_many :grades
has_many :teachers, through: :grades
end

But, as we already discussed, we can’t do the same for my family tree.

class Person < ActiveRecord::Base
has_many :people ??
has_many :people, through: :people ???
belongs_to :some_more_people
#what is going on
#i have no idea
end

Yeah… this looks like a mess. Okay, well something that isn’t totally wrong about it is that in a Family Tree, People do have_many People. And whenever A has_many Bs and B has_many As, we want some kind of joiner model. In this case, the relationships themselves will act as joiners. For my tree, I decided to only model “horizontal relationships” (marriages) and “vertical relationships” (‘filial’ relationships — parent/child). All other types of relationships can be deduced from these two, using instance methods. (E.G. if you want a list of all your siblings, query the list of all People to find those who share a Parent with you; Grandparents are just your Parents’ Parents; etc).

Cool. So this is what we’re cookin’ with so far:

class Marriage < ApplicationRecord
end
class Filial < ApplicationRecord
end
class Person <ApplicationRecord
end

Now for the climax of this post: the macros. Well, loosely speaking, I want some belongs-y things inside of Filials and Marriages. This is because a marriage can’t exist without the two people who are married, nor can a parent/child relationship exist without… well, the parent and the child. So these are dependent on the existence of the People to whom they belong. So, in a Filial for example, I could say:

class Filial < ApplicationRecord
belongs_to :parent
belongs_to :child
end

buttttt we don’t have such a thing as Parent and Child classes. So, that’s no good. Okay, so we could try this instead:

class Filial < ApplicationRecord
belongs_to :person
belongs_to :person
end

Okay, I mean, that makes more sense. After all, we have a Person class, and a Filial relationship does in fact belongs_to two different Person objects. So the problem? Well, which one is which? How could you tell the difference between the Person object who is the parent and the one who is the child? We need some way to fuse the previous idea with this idea together.

And here it is:

class Filial < ApplicationRecord
belongs_to :child, class_name: "Person", foreign_key: "child_id"
belongs_to :parent, class_name: "Person", foreign_key: "parent_id"
end

Here we have the best of both worlds. Now, we are able to keep track of which object is the parent, which is the child, and they are both Person objects. Also, because they are all Person objects, a particular Person might be identified as the parent in one Filial relationship and the child in other Filial relationships, without any problem at all. Perfect.

So now that we’ve got all of our relationships ironed out (at least here in the model, nobody said anything about IRL), the last part is figuring things out in the Person class. Not every person has a marriage or a child or even a parent relationship represented in a family tree. So since a Person’s existence doesn’t depend on the existence of one of these relationships, a belongs_to would not be appropriate. A Person can have several children and at least two parents (more than two if we include step-parents and adoptive parents as parents), so it seems like the appropriate association macros would be:

class Person <ApplicationRecord
has_many :parents
has_many :children
end

Butttt once again, that won’t work. AR won’t quite know what those mean, since there are no Parent and Child objects. We could instead do:

class Person <ApplicationRecord
has_many :filials
end

and while AR will now recognize this, trying my_guy.filials will always come up nil, because when leafing through Filial objects, it will be looking through parent_id’s and child_id’s, which won’t compare to a Person’s person_id the way we want them too. So instead, we need to do this:

class Filial < ApplicationRecord
belongs_to :child, class_name: "Person", foreign_key: "child_id"
belongs_to :parent, class_name: "Person", foreign_key: "parent_id"
end
class Person <ApplicationRecord
has_many :parent_relationships, class_name: "Filial", foreign_key: :child_id, dependent: :destroy
has_many :parents, through: :parent_relationships, source: :parent
has_many :child_relationships, class_name: "Filial", foreign_key: :parent_id, dependent: :destroy
has_many :children, through: :child_relationships, source: :child
end

This way we have explicitly told AR that when it looks through Filials for my parents, to compare my person_id against those child_id’s to see the relationships in which I am the child, and thus find the list of my parent_relationships, and through that, the list of my parents.

Tricky stuff! But at the end of the day, mastering this concept will help you to navigate the many gifts which ActiveRecord can bestow upon us through its Marvelous Macros for associations.

--

--

Zachary Dagnall
Nerd For Tech

Passionate about all sorts of things, from learning languages to serving the underserved, and much more. Currently developing my skills as a software developer.