Using custom relation queries to establish Friends and Friendships in Rails and ActiveRecord

Elizabeth Prendergast
5 min readOct 22, 2019

--

Image courtesy of Jack Moreh on freerangestock.com.

Thanks to the rise of social networking, most people are pretty comfortable with the idea of friend requests: User A sends a friend request to User B, User B accepts User A’s request, User A and User B are now friends. For my most recent project at Flatiron School, my partner and I took on the ambitious task of re-building a popular 2005 social networking site in Ruby on Rails, requiring us to explore this very concept — albeit in much greater detail than any non-developer would!

Building out these relationships turned out to be more challenging than a simple has_many and belongs_to relationship which I have become incredibly familiar with over the past six weeks. Curious to see how we did it? Let’s get to work.

The friend request/friendship paradigm requires three models: Users, Friend Requests, and Friendships. We defined and sketched our relationships as follows:

  • A User has many Friend Requests
  • A Friend Request belongs to a Requestor and a Receiver
  • A User has many Friends through Friendships
  • A Friendship belongs to two Users
Snippet from our domain model sketch (Miro is great for this )

Hmm — it looks like this data model requires the User model to have a relationship with itself. Things are about to get interesting!

Let’s start with Friend Requests. While a Friend Request will always belong to two users, it is clear that one user will be the initiator (i.e. ‘requestor’) of the request while the other will be the approver (i.e ‘receiver’). In our Friend Request model we can define this as follows:

class FriendRequest < ApplicationRecord   belongs_to :requestor, class_name: :User
belongs_to :receiver, class_name: :User
end

By appending class_name: :User to our belongs_to relationships, we are telling Rails that these newly defined requestors and receivers are actually of the class User, not a class in their own right. I like to think of this as a User being able to assume different ‘roles’.

Next, we need to echo these relationships from the User perspective.

class User < ApplicationRecord   has_many :friend_requests_as_requestor, 
foreign_key: :requestor_id,
class_name: :FriendRequest
has_many :friend_requests_as_receiver,
foreign_key: :receiver_id,
class_name: :FriendRequest
end

If we break down what’s happening here, a User can have many Friend Requests either as the Requestor or as the Receiver. Since we don’t ever need to display both types of requests in a mixed list of incoming/outgoing friend requests, this works well for our purposes. We are using the class_name key again to tell Rails that friend_requests_as_requestor and friend_requests_as_receiver are actually of class types Friend Request, and we are using the foreign_key key to tell Rails that the instance of User can be identified as a requestor/receiver by the requestor_id/receiver_id in the Friend Requests table.

Now onto Friendships, where things start getting a little tricky…

My original relationships for the Friend and Friendship models looked something like this:

class Friendships < ApplicationRecord   belongs_to :user
belongs_to :friend, class_name: :User
endclass User < ApplicationRecord has_many :friendships
has_many :friendships, foreign_key: :friend_id
has_many :friends, through: :friendships
end

While we’d like to tell Rails that a user can have many friendships as both a User and as a Friend, ActiveRecord doesn’t work like this. With the above, the second declaration of has_many :friendships actually overwrites the first declaration, allowing a User to only have friends and friendships when they are the User, and not the Friend. While I could have taken the same approach as I had for Friend Requests…

class Friendship < ApplicationRecord   belongs_to :friend_a, class_name: :User
belongs_to :friend_b, class_name: :User
endclass User < ApplicationRecord has_many :friendships_as_friend_a,
foreign_key: :friend_a_id,
class_name: :Friendship
has_many :friendships_as_friend_b,
foreign_key: :friend_b_id,
class_name: :Friendship
has_many :friend_as, through: :friendships_as_friend_b has_many :friend_bs, through: :friendships_as_friend_aend

… we would need an additional custom method to return all friendships for a single user, which would look something like this:

class User < ApplicationRecord   def friendships
self.friendships_as_friend_a + self.friendships_as_friend_b
end
end

Uck. This isn’t very DRY or readable! Which led to me to think…

As it turns out, there are many different ways of implementing the friend/friendship relationship in Rails and ActiveRecord. A quick Google search had me exploring all sorts of new concepts such as polymorphism, self-joins, self-referential association, inverse relationships, and more.

I selected the following solution which uses a custom relation query. Since Rails associations are built from Active Record Relation objects, we are actually able to use relation syntax to customise our relationships. This way I can tell Rails to look for a user’s friendships in the friendships table when the user’s ID appears as either friend_a_id or friend_b_id:

class User < ApplicationRecord   has_many :friendships, ->(user) { where("friend_a_id = ? OR friend_b_id = ?", user.id, user.id) }   has_many :friends, through: :friendshipsendclass Friendships < ApplicationRecord   belongs_to :friend_a, class_name: :User
belongs_to: :friend_b, class_name: :User
end

By using the -> { ... } block, we can pass in the instance of user and insert any typical Relation methods we would normally use when writing ActiveRecord queries — neat!

Ultimately, what I would like you to take away from this blog post is not just how to create a Friends/Friendships/Friend Request model in Rails and ActiveRecord. There are so many more ways of establishing relationships in Rails than just the typical Rails relational methods(has_many, belongs_to, has_one, etc.), and by doing a bit more research beyond what is taught at your university or bootcamp you will be opened up to a whole new world of possibilities.

For further reading on these topics, I would recommend:

--

--