the best way to implement a friendship model in rails
Asked Answered
F

1

4

I want to implement a user's friends system in my app so i found the rails space solution very nice, the idea there is to create two lines in the Friendships table : the first line for the sender invitation, and the second one for receiver

relation between users is setup with has_many association like this:

has_many :friendships
has_many :friends, :through => :friendships, :conditions => "status = 'accepted'"

method for accepting a user as friend is like this :

# Accept a friend request.
def self.accept(user, friend)
    transaction do
        accepted_at = Time.now
        accept_one_side(user, friend, accepted_at)
        accept_one_side(friend, user, accepted_at)
    end
end 

accept_one_side() method is :

# Update the db with one side of an accepted friendship request.
def self.accept_one_side(user, friend, accepted_at)
    request = find_by_user_id_and_friend_id(user, friend)
    request.status = 'accepted'
    request.accepted_at = accepted_at
    request.save!
end

this have advantage that we can executing one request to get all friends from the two side (either the user is who sent the invitation or the friend is who sent it)

but i think this have disadvantage if for example in reality there is 500 friends, Friendships table will contains "500X2 = 1000" lines

the second solution is to make a reverse association with has_many through like explained in RailsCast #163 Self-Referential Association :

has_many :friendships
has_many :friends, :through => :friendships
has_many :inverse_friendships, :class_name => "Friendship", :foreign_key => "friend_id"
has_many :inverse_friends, :through => :inverse_friendships, :source => :user

but here if you want to get all friends for a user you should use two request for that :

user.friends
user.inverse_friends

which is not the best way at all if you have a huge Friendships table ...

what i want to know is what is the best one method from the two above, then there is a way to optimize it ? if there is also another super method i will be thankful

Froehlich answered 12/10, 2013 at 21:52 Comment(0)
G
4

I would prefer the version that needs two connections between the friends, one for each direction. The reason is the same you mentioned: It allows more Rails-like queries on a user's friends.

Furthermore I think it would be clearer to have different tables for friendship request (one direction) and existing friendships (two directions)

Since you have a friendship model in the middle, I suggest to use the magic of callbacks. If you define some callbacks, it must be possible that you only have to take cake for one side of the connection, the callback should should be able to create (or delete) the matching complement.

# in friendship_requests
after_save :created_friendship

def accept
  update_attributes(:status  => 'accepted')
end

private
  def created_friendship
    sender.friends << receiver  if status_changed? && status == 'accepted'
  end


# in user.rb
has_and_belongs_to_many :friends, after_add:    :create_complement_friendship,
                                  after_remove: :remove_complement_friendship

private
  def create_complement_friendship(friend)
    friend.friends << self  unless friend.friends.include?(self)
  end

  def remove_complement_friendship(friend)
    friend.friends.delete(self)
  end

This is just a first idea, for sure some validators and callbacks are missing...

Gavrila answered 12/10, 2013 at 22:46 Comment(8)
Hi @Gavrila thank you for the answer, what do you think about table size which will be X2 ?Froehlich
Double table size will not be a problem for a long time. I have a friendship table in a production database has holds more than 4Mio entries and does not even take 200MB on disk.Gavrila
thank you, your comment reassures me, one more question about your code please what do you means by after_save :determine_friendshipFroehlich
Fixed that to after_save :created_friendshipGavrila
can't understand role of this callback !Froehlich
My idea was that a sender send a request to a receiver (friendship_request). The receiver accepts the friendship_request (accept). The accept method triggers the after_save that creates the first real friendship, what triggers again a second callback for the complement_friendship.Gavrila
ok thank you for share with me some code and experimentation i appreciate your experience with 200MB of database :D in rails space book author use transaction rather than callback to be sure that the 2 relations will be created, your code have also some good idea thank'sFroehlich
From the docs: The entire callback chain [...] runs within a transaction. That includes after_* hooks.Gavrila

© 2022 - 2024 — McMap. All rights reserved.