Follow Me, Follow You, Self Join!

Josh M.R. Allen
6 min readOct 1, 2020

Far and wide, you’ll find only just a few samples of self joins. The first source you’ll probably go to is the Active Record documentation.

A famous example of a self join in social media is Twitter’s follow system. I only realized how novel a pattern this is after finally implementing my first self join for a project I did recently with collaborator, Jen, on a project we did using vanilla javascript and a rails backend api.

The best way to get the hang of these is to make them. To follow along here, you need to know some Ruby, be familiar with Ruby on Rails, and know what an API is.

Step 1

Open a terminal session and start a new rails project:

https://carbon.now.sh/

In most circumstances where you plan to make calls to your API, you then uncomment the ‘rack-cors’ gem found in GemFile and then navigate to the config/initializers/cors.rb file to uncomment the code block in there and replace ‘example.com’ with an asterisk (*).

I’ll leave screenshots of that out because I want to focus on demonstrating the self join.

Step 2

Create a User model and controller. The most convenient way to do this is with the generator:

https://carbon.now.sh/

Create a Follow model — don’t worry about anything else unless you find you really need it. For now, I’ll just use the model generator to keep things simple:

See below where I add indexes to the migration file itself.

Notice that I added the two columns, follower_id and followee_id. Recall that our purpose is to create a self join, meaning that we have one model that is literally joined to itself — or rather, instances of that one class are labeled as one “thing” having many of “another thing” that just so happens to be an instance of the same class.

You’ll see in the following steps how Rails actually provides some macros that makes this easy — they just aren’t very easy to find in the documentation and there aren’t that many articles about them out there (and if you do find a few, they tend to be a few years and a few deprecated methods out of date).

One might be tempted to use a specific migration generator that uses a ‘create_join_table’ which you can find in the Active Record documentation pretty easily.

The problem we encountered with trying to do this was apparently some confusion that Rails had using ‘create_join_table’ and t.references not being able to effectively see its “reference.”

At the writing of this article, I’m actually still confused about this, so please comment/reach out if you have a better explanation or correction for me — or at least a link to one.

Step 3

Set up the relationship between user and … well, user.

I think in other situations, this might not be so confusing, but we had a hard time with this one, simply due to the naming that we were using. You’ll see why — just keep reading:

In app/models/user.rb, add the following associations:

Notice the class_name macro which identifies the model, Follow.

Here’s what I added as comments in our project so that I wouldn’t forget and make a mistake that would cost us another day:

# follower_follows “names” the Follow join table for accessing through the follower association

# source: :follower matches the belong_to :follower identification in the Follow model

# who follows the user; to see all a user’s follows: simply use ‘user.follower_follows’ this is what a twitter user would use to view their followers

There’s more!

# followee_follows “names” the Follow join table for accessing through the followee association

# source: :followee matches with the belong_to :followee identification in the Follow model

# who the user follows; user.followee_follows shows all the users that this particular user follows

Now tell the Follow table who it ‘belongs_to’ in app/models/follow.rb:

Notice the class_name macro identifying the User model.

Not quite done yet, we still have a few more steps.

Step 4

Add indexes to the Follow migration file.

The migration file was created when we used the ‘rails g model Follow’ command. We gave it two columns, follower_id and followee_id, both as bigints. For more information on bigint, and why/when to use it, see this discussion here and this stackoverflow article here.

‘index’ is pretty important here. See this article for more information.

This was probably the most confusing part — I had no idea what was happening here with index and the brackets identifying ‘follower_id, followee_id’ and ‘followee_id, follower_id’ respectively.

Standard Rails practice is to add an index to any foreign key column. We’re essentially telling Rails that a follower_id can also appear as a followee_id on the same table as well as the reverse. I’m still looking for a better way to explain this and if you can, please provide feedback!

Step 5

Add a method for ‘following’

I found that the best way to handle associating users with each other was to create a new Follow instance, where the user’s id would be used for follower_id and the id of the other user to be followed (a.k.a, the followee) would be used for followee_id:

Step 6

Set up a simple serializer for user:

You can also use the active_model_serializer gem to simplify this a lot, but I thought it would be fun to show the follows in action.

Step 7

Get your UsersController ready to go:

Step 8

Seed the database, but don’t forget to ‘rails db:create’ and rails db:migrate before rails db:seed.

Step 9

Run ‘rails s’ in your terminal and visit localhost in your browser. My port is set at 3000 and I use the Chrome extension, JSON Viewer Awesome to help retain my focus when looking at my own api response:

Any reference to any persons or fast food chains are purely coincidental.

Step 10

Step back and admire the beauty of this, but also take a moment to question a few things and dive deeper into some of the rails conventions needed to implement self joins.

Any advice or feedback would be greatly appreciated — especially if I inadvertently ignored best practice or if there’s a much easier way to implement the relationship. I’m still looking for that migration method that I don’t know of yet.

References:

explanation of indexes:

https://medium.com/@mera.stackhouse/what-are-indexes-and-how-to-add-them-to-your-rails-app-dc066d538771

https://www.ducktypelabs.com/when-is-indexing-in-a-model-appropriate/

generator cheat sheet:

https://medium.com/@kevinyckim33/rails-generators-cli-cheatsheet-711295e7a1ed

for an explanation and conversation about bigint vs int:

https://gitlab.com/gitlab-org/database-team/team-tasks/-/issues/18

https://stackoverflow.com/questions/38053596/benchmark-bigint-vs-int-on-postgresql

--

--

Josh M.R. Allen

Crazy cool like a mouth full of 花椒 (Sichuan peppers)! Software Engineer | Rails | React | JavaScript | Ruby | AWS S3 | Proud father of a precocious explorer.