Looking for advice on table/model relationships for "adding friends", user-to-user relationship

Hi folks. I am developing a simple system where users can add other users as friends. I have everything working, but after poring over Agile Web Development With Rails, I feel like there must be a more Active Recordy way of going about it. Something just seems off. Here's what I've got

A User model and associated users table that contains all the typical user stuff (name, address, etc). The User class has_many :friends.

A Friend model and table that contains two columns: user_id and linked_user_id. The Friend class belongs_to :user.

So for example if I have user 1 and user 2, and user 1 wants to add user 2 as a friend, I do the following:

friend = Friend.new friend.user_id = session[:user_id] # session[:user_id] is the source user, 1 friend.linked_user_id = params[:id] # params[:id] is the target user, 2 if friend.save    render :whatever end

Then, when I want to get a user's list of friends as user objects, I make a call to an instance method in the User class as follows:

def linked_users     users =     friends.each do |v|       users << User.find(v.linked_user_id)     end     users end

Can anyone spare any advice? Much appreciated. Cheers

I wanted to note that in addition to this seeming like too manual a process, it runs a sql statement for every user rather than one join. I tried the following declaration in the User model but it didn't work:

has_many :users, :through => :friends, :foreign_key => "linked_user_id"

I have implemented a friendship system, which happens to be polymorphic, but that is not really relevant. This handles invitations and the status of the invitation as well (open, accepted, rejected).

It requires a table called friends (ignore the ..._type fields if not polymorphic)...

  CREATE TABLE friends (     id serial NOT NULL,     inviter_id integer,     inviter_type character varying(255),     invitee_id integer,     invitee_type character varying(255),     status integer DEFAULT 0,     created_at timestamp without time zone,     updated_at timestamp without time zone );

Here is what would go into the User model....

       this_class_name= 'User'

       has_many :invites,           :class_name => "Friend",           :as => :inviter,           :order => 'created_at DESC',           :dependent => :destroy

        has_many :outgoing_invites,           :class_name => "Friend",           :as => :inviter,           :conditions => "friends.status = 0",           :order => "created_at DESC"

        has_many :accepted_invites,           :class_name => this_class_name,           :through => :invites,           :source => :invitee,           :source_type => this_class_name,           :conditions => "friends.status = 1"

        has_many :invites_to,           :class_name => this_class_name,           :through => :invites,           :source => :invitee,           :source_type => this_class_name,           :conditions => "friends.status = 0"

        has_many :rejected_invites,           :class_name => this_class_name,           :through => :invites,           :source => :invitee,           :source_type => this_class_name,           :conditions => "friends.status = -1"

        has_many :rejections,           :class_name => "Friend",           :as => :invitee,           :conditions => "friends.status = -1",           :order => "created_at DESC"

        has_many :invitations,           :class_name => "Friend",           :as => :invitee,           :order => 'created_at DESC',           :dependent => :destroy

        has_many :incoming_invitations,           :class_name => "Friend",           :as => :invitee,           :conditions => "friends.status = 0",           :order => "created_at DESC"

        has_many :accepted_invitations,           :class_name => this_class_name,           :through => :invitations,           :source_type => this_class_name,           :source => :inviter,           :conditions => "friends.status = 1"

        has_many :invitations_from,           :class_name => this_class_name,           :through => :incoming_invitations,           :source_type => this_class_name,           :source => :inviter

        has_many :rejected_invitations,           :class_name => this_class_name,           :through => :invitations,           :source => :inviter,           :source_type => this_class_name,           :conditions => "friends.status = -1"

An inefficient method to get all friends is...

    # finds friends regardless of who was inviter or invitee, and status is accepted     def friends       accepted_invites + accepted_invitations     end

A more efficient (untested) method is...

  def friends      find_by_sql('SELECT p.* FROM users p, friends f WHERE f.status = 1 AND ( (f.invitee_id = #{id} AND p.id = f.inviter_id) OR (f.inviter_id = #{id} AND p.id = f.invitee_id) )')   end

The advantages of this method are that only one entry per friendship is required in the friends table (rather than two each way).

I'll be blogging in far more detail on this soon in my blog...

http://blog.wolfman.com

Hope that helps

Lots to look over, as your system is a lot more in depth than mine, but I'll pore over it tonight. Thanks wolfman. :slight_smile:

have you considered using acts_as_tree? then, for instance you could do:

some_user.friends << some_other_user

-mike

Mike, give you give me a few more details? Cheers.

Check it out in your book in section 18.6. Acts As Tree is designed for creating a hierarchy of objects but it's basically a Has And Belongs To Many relationship with itself so it should work well for your purposes.

Once you get the relationship set up you can treat the friends as an array on each user like:

#grab some users from the database mike = User.find_by_name("mike") rebecca = User.find_by_name("rebecca") yngwie = User.find_by_name("yngwie") derrida = User.find_by_name("derrida")

#add friends like this: mike.friends << rebecca mike.friends << yngwie mike.friends << derrida #notice everyone wants to be my friend?

-Mike

Hmm not sure if that's going to work. acts_as_tree looks to be designed for classes that have only one parent (belongs_to rather than has_and_belongs_to_many).

acts_as_tree is in no way a HABTM, it's a has_many / belongs to: parent has many children, children beloong to 1 parent. so not suited for this. there's a plugin for this purpose however: http://blog.dnite.org/2007/6/8/howto-has_many_friends

Hi folks. Got this solved today (huge massive glowing happiness) and thought I'd post my solution. Note, this is more of a subscription model than a friends model.

Class User   has_many :bookmarks   has_many :bookmarked_users, :through => :bookmarks, :source => :bookmarked_user end

Class Bookmark   belongs_to :user   belongs_to :bookmarked_user, :class_name => "User", :foreign_key => "bookmarked_user_id" end

And the join table migration looks like this.

class CreateBookmarks < ActiveRecord::Migration   def self.up     create_table :bookmarks do |t|       t.column :user_id, :integer, :null => false       t.column :bookmarked_user_id, :integer, :null => false     end   end

  def self.down     drop_table :bookmarks   end end

Works perfectly. Enjoy!