Rails: Self-Referential Tables

Simple relationships in rails were easy. Things belonged to stuff and other things had one or many of something else. No big deal. I just drew an arrow when mapping out my relationships on paper showing what belonged to what and moved on. But then I wanted to create an application where users could send each other messages. “No big deal!” I thought, “Users have many Messages and Messages belong to Users!”

But, despite my hopeful thoughts and well-wishes, it wasn’t that simple. A message belonged to two users. One user was sending a message to another user. How did I even go about this? It went against everything I had learned about rails relationships up to this point. That’s when I stumbled upon something called a “self-referential relationship.”

It shouldn’t have surprised me. Self-referential relationships are obscenely common in social media. Websites like LinkedIn, Twitter, Facebook, Airbnb, and Medium all rely on them. Still not sure what I’m talking about? Connections, messages, followers, friends, fans, and hosts, are just some of the ways you can utilize self-referential relationships!

Let’s go back to my users/messages example. We start out by creating a migration for the messages table that has a message, a sender id, and a recipient id. The message is just the text that the sender writes, but the sender_id and the recipient_id should be unfamiliar to you! I’ll give you a hint: They’re both ids that belong to a user in the User table!

//create_messages.rbclass CreateMessages < ActiveRecord::Migration[6.0]
def change
create_table :messages do |t|
t.text :message
t.integer :sender_id //user #1 foreign_key
t.integer :recipient_id //user #2 foreign_key

Next we go over to the Message model. We’re going to tell the model that it’s going to have two different things it belongs to, a Sender (user #1), and a Recipient (user #2). They’re both coming from the User class, but they’re going to have two foreign_key names on the messages table. The reason we do this is so that we can query active record whenever we want so that we can see which user was the sender and which user was the recipient.

//message.rbclass Message < ApplicationRecord
belongs_to :recipient, class_name: "User", foreign_key: "recipient_id"
belongs_to :sender, class_name: "User", foreign_key: "sender_id"

We then go over to the User model and do the inverse of what we did before. The User model is now aware that it has many messages AND many sent messages! If you’re still confused, you can do User.first.sent_messages and User.first.received_messages to see two completely different arrays of messages that belong to the first user!

//user.rbclass User < ApplicationRecord
has_many :received_messages, class_name: "Message", foreign_key: "recipient_id"
has_many :sent_messages, class_name: "Message", foreign_key: "sender_id"

And, just like that… you’re done! It wasn’t that easy for me to figure it out, but I really hope I explained it well enough for you to understand it. Just in case you’re still struggling, here’s a few more examples for you to see!

class Friendship < ApplicationRecord
belongs_to :user
belongs_to :friend, class_name: 'User'
class Follow < ApplicationRecord
belongs_to :following, class_name: 'User', foreign_key: 'following'
belongs_to :follower, class_name: 'User', foreign_key: 'follower'
class FriendRequest < ApplicationRecord
belongs_to :user
belongs_to :friend, class_name: 'User'

While I don’t think it’s super duper complicated any more, I did find the concept completely impossible to understand at first and I really hope this helps you just a little bit!

Full-Stack Developer, Software Engineer, and UX/UI Aficionado

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store