SmartFunnyCool

Writing about code

Friendships in Active Record


While working on Guten Dog my partners and I struggled with adding friendships to our User model via Active Record. For our app we needed to have mutual friendships. That is, Facebook-style friendships where a confirmed friendship is a reciprocal relationship (and different from the follower model of something like Twitter). While our final solution was not terribly complex it involved breaking out of the Active Record box and actually writing some association methods ourselves.

The difficulty lies in writing a join table that references two users reciprocally. At first I did not want to keep track of who initiated the friend request since it does not matter to the relationship once a friendship is confirmed. However, keeping track of this in the database and hiding this from the user works well. And after struggling to use complicated aliasing I found that a few custom ruby methods worked nicely. Below is my step-by-step guide for adding friendships to a User model.

WARNING: While this will add friends to your Active Models there is NO GUARANTEE that this will help you find friends IRL.

Step 1: Create Friendships Table

To keep track of the friendships association we create a friendship table. The friendship table will reference the user who created the friendship as the "user" and it references the potential new friend as "friend." We also need to include a column that tracks whether the relationship is confirmed. Using the rails helper we can type:

rails g model friendship user:references friend:references confirmed:boolean  

This will create a friendship model and migration. First we need to edit the migration so that the table does not attempt to reference the non-existent friends table. Change your migration file so that it looks as follows:

class CreateFriendships < ActiveRecord::Migration  
  def change
    create_table :friendships do |t|
      t.references :user, index: true, foreign_key: true
      t.references :friend, index: true
      t.boolean :confirmed

      t.timestamps null: false
    end
    add_foreign_key :friendships, :users, column: :friend_id
  end
end

Adding the foreign key as we do above allows us to reference as user as a "friend" in this table. With this change we are ready to migrate the database

Amend the Belong To Relationships in the Friendship Model

While the above migration creates a database that can store a user id in a friend_id column, we must also make a similar change in the Friendship model. While a friendship belongs to a user and a friend, a friend is really just a user by a different class name. Edit the Friendship class so that it looks as follows

class Friendship < ActiveRecord::Base  
  belongs_to :user
  belongs_to :friend, :class_name => "User"
end  

Create Has Many Relationships in the User Model

Since a Friendship belongs to a user and a friend we must set up each of these has many relationships. While including the line has_many :friendships covers all of the friendships that a given user initiated we also need to keep track of the friendships where the user is in the "friend" column. We can call these inverse friendships and we must explicitly name their foreign key (since there is no inverse friendships table):

class User < ActiveRecord::Base  
  has_many :friendships
  has_many :inverse_friendships, :class_name => "Friendship", :foreign_key => "friend_id"
end  

Create a friends method in the User Model

Since friendships and inverse friendships have equal weight once they are confirmed, we need to create a method that returns all of the other users who are connected to the given user via a confirmed friendship or confirmed inverse friendship. We can accomplish this with the following method

  def friends
    friends_array = friendships.map{|friendship| friendship.friend if friendship.confirmed}
    friends_array + inverse_friendships.map{|friendship| friendship.user if friendship.confirmed}
    friends_array.compact
  end

Other Helper Methods for Friendships

We also will probably want helper methods to keep track of all of a user's pending friends and incoming friend requests:

  # Users who have yet to confirme friend requests
  def pending_friends
    friendships.map{|friendship| friendship.friend if !friendship.confirmed}.compact
  end

  # Users who have requested to be friends
  def friend_requests
    inverse_friendships.map{|friendship| friendship.user if !friendship.confirmed}.compact

And last we will likely want to have methods to confirm a friend request and check to see if a given user is a friend

  def confirm_friend(user)
    friendship = inverse_friendships.find{|friendship| friendship.user == user}
    friendship.confirmed = true
    friendship.save
  end

  def friend?(user)
    friends.include?(user)
  end

So our User model looks as follows:

class User < ActiveRecord::Base  
  has_many :friendships
  has_many :inverse_friendships, :class_name => "Friendship", :foreign_key => "friend_id"

  def friends
    friends_array = friendships.map{|friendship| friendship.friend if friendship.confirmed}
    friends_array + inverse_friendships.map{|friendship| friendship.user if friendship.confirmed}
    friends_array.compact
  end

  # Users who have yet to confirme friend requests
  def pending_friends
    friendships.map{|friendship| friendship.friend if !friendship.confirmed}.compact
  end

  # Users who have requested to be friends
  def friend_requests
    inverse_friendships.map{|friendship| friendship.user if !friendship.confirmed}.compact
  end

  def confirm_friend(user)
    friendship = inverse_friendships.find{|friendship| friendship.user == user}
    friendship.confirmed = true
    friendship.save
  end

  def friend?(user)
    friends.include?(user)
  end
end  

The above is a straightforward backend setup for a friendship model to allow users of a rails app to have mutual friendships.

Share this post: