Self-Referential Relationships (aka Self-Joins) in Rails

Dick Ward
4 min readFeb 25, 2018

--

Rails relationships were one of the hardest things for me to wrap my head around while I was learning web development, but now I’m in love with the amount of different ways they can be set up. Last week, I explored how you can create multiple relationships between two models by using separate join tables. This week I’m going to look at self-referential relationships and all the fun we can have with them.

We can see self-referential relationships practically everywhere in the wild. Twitter followers, Facebook friends, Linkedin connections, and Medium fans are all examples of self-referential relationships. Each of these relationships can be set up just two tables: the user table, and a join table to link it to itself.

For this example let’s create a rails app and generated a resource called ‘user’. We want to keep this simple for now, so let’s give a user a name and call it a day. We can also jump into our Rails console at this point and create some users.

Creating some users in the Rails console

So now we’ve got a few users and we want them to be able to follow each other. We want users to have many followers and we want them to be able to follow many other users, so it’s a many to many relationship. It’s going to work like any other many-to-many relationship you’ve used in the past, but the setup is going to be a little different.

Our next step will be creating a join table. Just like any basic join table, we’re simply including two ids for the table to know about. We’ll call this one ‘follows’ and it will have two integers, a follower_id and a followee_id. It’s important that the naming be clear here and the self-referential relationship can be very confusing otherwise.

Now it’s time to look at the models. This is where the magic happens. We start with the follower model, and the code looks like this.

class Follow < ApplicationRecord
belongs_to :follower, class_name: 'User'
belongs_to :followee, class_name: 'User'
end

What we’re doing here is telling Rails that even though we’re looking for something called a follower_id, what we actually want is a user_id. Same thing for followee. Next up, the user model. Let’s approach this one at a time.

has_many :followed_users, foreign_key: :follower_id, class_name: 'Follow'has_many :followees, through: :followed_users

The first line of this has many through relationship is the one that’s likely to cause a little confusion so let’s break it down. We’re saying that a user can have many followed_users, meaning that they can follow as many people as they want. We’re going to use the foreign key follower_id to represent these followed_users and this can be found in the Follow class. Go through it a time or two to make sure you get what’s going on, because it’s clear sailing after this.

The second line of code is exactly what you’d expect to see in any has_many_through relationship. We’re saying that we have many followees through the followed_users that we’ve defined above. Now we do the same the other way around.

as_many :following_users, foreign_key: :followee_id, class_name: 'Follow'  has_many :followers, through: :following_users
Establishing the relationship in Rails console

And as simple as that, we’re ready to go. Pop up your rails console and push in one user as a follower of a different one. You’ll see that the reverse relationship is created as well.

Followers and followees in Rails console

Just like that, you’re done! From here, the self-referential relationship is yours to play with. For starters, you’ll want to make sure that a user can only follow another user once. After you’ve got that handled, try and think of some different things to do with that join table. Maybe each user can give a rating to the users they follow, or a review. Or try setting up something like Twitter, where a user can see the tweets of another user in their feed by following them. Or something completely different!

--

--

Dick Ward

Full-stack web developer with a flair for the theatrical and a mission to leave the world a better place than I got it. DickWard.com