Self-Referential has_and_belongs_to_many Relationships, and Why It Doesn’t Do Enough

Jack Hilscher
4 min readApr 10, 2019

--

In object-oriented languages, it is necessary to create a relationship between objects so they communicate information between each other. In Ruby on Rails, relationships are made through join tables, and objects are saved into databases, including objects on said join tables.

Imagine you are writing a program, a game, where the user (an instance of the Player class) can interact with other users (in this case, an instance of the Other Player class), and form Friendships. Below are the tables with the required information to make a relationship.

Each user, a Player, and an Other Player, has a name. The Friendship table references both ids of the two users, plus some additional information, should they want to add those details.

Above I created the models to represent the instances of the Players, Other Players, and Friendships. The Friendship table and model act as a joiner between the two users, creating an instance of the Friendship class with both the Player and the Other Player represented in the Friendship join_table. A Player has many Friendships, and has many Other Players through those Friendships, and vice-versa. A Friendship belongs to both a Player and an Other Player.

Above is an example of a Friendship between the Player instance “Jack” and the Other Player instance “Wayne”.

However, we could make this relationship even simpler and cleaner, removing the join_table model altogether! Let’s discuss the “has_and_belongs_to_many” relationship!

This type of relationship has the same functionality as a has_many through relationship, but we are referencing the other table and model directly. There is no need to have a belongs_to relationship, because the join_table model does not need to exist. This relationship is perfect for a single many_to_many relationship between two classes. Creating this relationship looks like we have less code to write because we don’t need a model of our join_table! The join_table still needs to be created in the database, but the model itself is not necessary; the join_table itself only needs to be referenced for the relationship to occur. It is easy to add Wanye to Jack’s friends.

But wait, aren’t all Other Players also….Players? They are all users in this game! What happens if Wayne wants to become friends with someone else? Would we have to make another instance of Wayne on the Player table? It would be more appropriate for Players to be the only model, and there needs to be a way for an instance of the Player model to become friends with another instance of the Player model. Let’s discuss a self-referential relationship.

In a self-referential relationship, instances of a model have a relationship with another instance of the same model, but through a join_table. I wonder if you could use this relationship in a has_and_belongs_to_many relationship…

You can! There are a few more lines of code, but everything is written in only one model because the relationship is between instances of that one model. Through this self-referential has_and_belongs_to_many relationship, the user Jack can be friends with the user Wayne. The added lines of code reference the Player instance that is not referenced as self, the id of the other Player instance, the join_table, the method called to reference this relationship. This is very simple, so let’s see what it takes to make a self-referential relationship in a has_many through relationship.

Whoa! That is a lot more information, and you need to have two models! Let’s break down this code.

A user (Player) has a friendship list, where they list all of the other users they are friends with. So they have access to that friendship_list method and the friends method. Meanwhile, all other users can know whose friendship lists they are on, and know that those other players will be friendly to them. So they have access to that on_friendship_list method and the friendlies method. It is important to name your methods so they make sense for the instances. In this case, the same relationship occurs between two separate users, essentially, so you have to delineate the method names so you aren’t calling the wrong method on a Player instance.

So which is easier to read? The clear winner is a has_and_belongs_to_many relationship, right?

In my opinion, not really. A has_and_belongs_to_many requires much less code, but at a drawback of functionality.

  1. The number of methods you can call to reference your join_table is…none! You can only access your listed Friends.
  2. There is no join_table model, so there are no additional methods you can create or access in that model, like the friendship_list or friendlies methods.
  3. Most importantly, since there is no join_table model, even if you have other columns in the join table in the database, there is no way to write or read them. So a length_of_friendship_years column or a friendship_name column would be inaccessible with this relationship.

In conclusion, though a has_and_belongs_to_many relationship seems more elegant to write, it comes at a significant drawback. When used in self-referential relationships, it gives much less access to a Player instance and other instances of its own class that it's connected to. It is important to understand different ways to connect instances, but the has_many through relationship is more versatile and flexible.

--

--