has_many :selves?

Zack Wilder
Jul 25, 2017 · 5 min read

Let me paint a picture for you: You’re in the library, and suddenly you hear loud yelling. You sprint to see what all the commotion is about, and as you turn the corner into the Reference section, you see two hulking men in orange and yellow spandex weight-lifting suits — one is power-squatting an overweight librarian, and the other is cheering him on with animal-like grunts.

The two men — let’s call them Surge and Vladamir for now — are clearly fish out of water. The Reference section is no more of a place to squat librarians than the gym is a place to snuggle up with your favorite book and a cup of hot cocoa. And yet it’s perfectly normal for one person to do both of these activities in the course of his day, as well as serving as a friend, spouse, professional, volunteer, etc.

Most difficulties in our relationships come when we assume or expect a behavior that is ill-suited for the other person or our environment. Our actions are infrequently intrinsically wrong, but they are frequently misplaced. For example, it’s not wrong to be a laser-focused, highly-demanding person if you’re a surgeon and someone’s life is on the line. But that sort of behavior is at best misplaced (and at worst, damaging) when you’re a little league soccer coach. It may be highly productive to rigorously analyze a problem at your job as a statistical economist, but taking the same approach with a spouse who just wants you to listen promises disaster. And it goes without saying that there’s nothing wrong with power-squats per se, but doing them at the library — well — that just seems a bit off.


Let’s think about this as it relates to programming. Recently, while working on an app to mimic the behavior of AirBnB, a friend and I ran into an interesting challenge: the same user, at one time or another, could be a traveler, a host, a companion, a reviewer, a review-e, one who sends a message, and one who receives a message. How would we model this without creating a mess of unnecessary data and associations, or (like the intro above) expecting behavior that doesn’t — or can’t — exist? As we thought about it further and discussed with others, we realized the challenge was not isolated to AirBnB, it was present on Twitter, too: a person could be a follower, be following, and be a post-er. On Facebook, users could ‘like’ and be ‘liked’. They could support a cause or create a cause. That left us wondering: on AirBnB and in general, how would we handle complex associations where a user could potentially belong_to :self and a user could have_many :selves?

Before tackling this challenge, let’s first bring ourselves back to the present tense for the sake of grammatical ease.

Ahhh, much better.

The first step in solving this problem is to remember that Ruby’s has_many and belongs_to associations are just sweet syntactic sugar that takes educated guesses. If we, the programmers, need something different, we have to provide direction. And in the case of our AirBnB user, it looks a little something like this:

class User < ActiveRecord::Base
has_many :listings, foreign_key: "host_id"
has_many :reservations, through: :listings
has_many :reviews, foreign_key: "guest_id"
has_many :companions, foreign_key: "companion_id"
has_many :trips, class_name: 'Reservation', foreign_key: 'guest_id'
has_many :messages_from, class_name: "message", foreign_key: "from_user_id"
has_many :messages_to, class_name: "message", foreign_key: "to_user_id"
end

Once you’ve picked yourself off the floor, let’s go through a few of these associations slowly and think about the underlying SQL code that makes them happen.

We see from the has_many :listings, foreign_key: "host_id" association that when a User serves as a host, he or she can have many property listings. The way to account for this is to simply place a host_id column in the Listings table which corresponds to the user_id in the User table. Now when we want the User to function as a host and display his listings, we can type user.listings to execute the following SQL:

SELECT name FROM listings WHERE self.id = host_id

Not so bad! Let’s look at the reciprocal association on the listings table:

class Listings
# ...
belongs_to :host, class "User"
end

This tells us that a listing belongs to a host which can be identified by looking at the User table and running the following SQL:

SELECT name FROM users WHERE host_id = user.id

If we hadn’t told the Listings class to look in the User class for it’s host, it would, by default, look for a Host class and come up empty.

Let’s take a look at another example:

class User
#...
has_many :messages_from, class_name: "message", foreign_key: "from_user_id"
has_many :messages_to, class_name: "message", foreign_key: "to_user_id"

The key to understanding what’s going on here is to remember that has_man and belongs_to are just methods that take arguments with names of our choosing. If we follow convention, Rails will do a lot of work for us, but if we don’t (or can’t), we — just like our friends Surge and Vladamir — have to do the heavy lifting.

In this example, we want to be able to show the messages that a user has sent or received. For the sake of readability, we want to call this method :messages_from or :messages_to, but that’s not going to clue Rails in on how to perform a SQL query (unlike typing, for example, :cars, which would SELECT * FROM cars WHERE user_id = self.id). What is a developer to do?

Here’s what we do: we tell the super-duper has_many method creator to create a method called messages_from that should look in the Message model for a foreign key of from_user_id that matches user.id. We also create a method called messages_to that should look in the Message model for a foreign key of to_user_id that matches user.id. Simple! The two SQL queries would look like this:

SELECT message_content FROM messages WHERE self.id = from_user_id
SELECT message_content FROM messages WHERE self.id = to_user_id

See that! Nothing to fear, especially when you think about what thehas_many and belongs_to methods do, and what the underlying SQL query becomes. Let’s wrap up with their corresponding associations:

class Message < ActiveRecord::Base
belongs_to :to_user, class: "User"
belongs_to :from_user, class: "User"
end

What I enable myself to do here is take an object from my database and call the :to_user method on it, which returns the User object with a user.id that matches the to_user_id on my messages table. Here’s the SQL query:

SELECT * FROM messages WHERE to_user_id = user.id
SELECT * FROM messages WHERE from_user_id = user.id

To conclude: our users will frequently take on different rolls according to different use-cases of our applications. This behavior is not only doable — it’s natural — and a proper understanding of Ruby’s magical methods can make it happen.


PS: If you’re wondering what the picture at the beginning of this post is supposed to represent, it should be obvious by now — the picture has_many :rolls 😄

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade