Looking for advice on table/model relationships for "adding

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 D. 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).

On Aug 26, 7:54 pm, “Michael Bannister” [email protected]

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!