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.