has_many :through Self-referential Example
Nov 10, 2006
While using an association table for the first time with Ruby On Rails I had a bit of trouble finding an easy to understand example of has_many :through. I needed to build a self-referencing table where People could have other People as friends through a Friendship. I was getting “stack too deep” errors, “could not find the association” errors, and also crashing Webrick before I figured out the correct setup. Here’s how I did it:
# the people table create_table :people do |t| t.column :name, :string end # the friendships association table create_table :friendships do |t| t.column :person_id, :integer t.column :friend_id, :integer t.column :authorized, :boolean, :default => false end class Friendship < ActiveRecord::Base # don't have to give class_name or foreign_key b/c ActiveRecord reflection works here belongs_to :person # make sure to give class_name and foreign_key b/c ActiveRecord doesn't know what friend is belongs_to :friend, :class_name => "Person", :foreign_key => "friend_id" end class Person < ActiveRecord::Base # tell ActiveRecord that a person has_many friendships or :through won't work has_many :friendships # create the has_many :through relationship has_many :friends, :through => :friendships # an example of how to get only the authorized friends has_many :authorized_friends, :through => :friendships, :source => :friend, :conditions => [ "authorized = ?", true ] # an example of how to get only the unauthorized friends has_many :unauthorized_friends, :through => :friendships, :source => :friend, :conditions => [ "authorized = ?", false ] end
I’ve been scratching my head over this exact problem for hours. Thank you so much for such a clear, well-written example. You have saved me days of work.
Thanks so much. Always nice to hear when an example helps someone.
Many thanks for your great example!
Glad it helped you Jason!
Hi John, thanks again for the code. I seem to be doing something wrong as I can’t get access to the freind array.
Could you give an example of how to create a new friend (without authorization).
I have in my controller:
def new @friends = @session['user'].friends #make sure the friendship is not self-friendship if @session['user'].id == @params['friend_id'] flash[:notice] = ‘You are already your own friend!’ redirect_to :controller => ‘user’, :action => ‘welcome’ #and make sure there isn’t already a friendship elsif @friends.find(:first, :conditions => [ "friend_id = ?",@params["friend_id"].to_i]) flash[:notice] = ‘You are already friends!’ redirect_to :controller => ‘user’, :action => ‘welcome’ else newFriend = User.find(@params[ââ¬â¢friend_idââ¬â¢]) @friends << newFriend flash[:notice] = ââ¬ËAdded friend!ââ¬â¢ redirect_to :controller => ââ¬Ëuserââ¬â¢, :action => ââ¬Ëlistââ¬â¢ end endAlthough I am getting an application error on the first line.
@friends = @session['user'].friends
How are you meant to access the array?
Many thanks,
Jason
Jason, I actually use the example in some code of mine and get a list of the person’s friends using @current_person.friends. @current_person is the user that is logged in and RoR does its magic to pull back all the friends using the friendship reference table. What exactly is the error you’re getting when you make that call?
To truly add a friend to the person you’ll need to add another entry to the friendship table. Something like this:
There might be some spiffy way that RoR can figure out this linkage but I know this works. Is that what you were looking for?
Hi John,
Thanks for the reply! I am getting an Application error:
Application error
Change this error message for exceptions thrown outside of an action (like in Dispatcher setups or broken Ruby code) in public/500.html
maybe it is because there are no friends?
Hi again,
It is really strange. If I try to access friends through the current person, in my case @session['user'].friends I get an application error like above.
I may just have to hard code it all with a couple of inner joins or something.
Jason, are you running the code with the RAILS_ENV as development? If you’re in the development environment and you reference a nil value it will tell you that. If you’re getting the generic 500 error then something else is wrong. Sometimes when you make changes to your models like the self-referencing you need to restart your web server for the changes to take place.
Hi there,
I couldn’t work out what was wrong with my previous problem, so I have rewritten the friendship. I wanted to be able to make a friend request, and to have it confimed before being added as a friend.
Here is is:
There are two tables in the database
user:
id(int)
name(varchar)
friends_users
id(int)
user_id(int),
friend_id(int),
requestfromuser(int)
requestfromfriend(int)
This is by no means a smooth or quick method, it was hacked together, so use at your own risk!
create the controllers and models
“ruby script\generate model user”
“ruby script\generate model friends_user”
“ruby script\generate controller user”
“ruby script\generate controller friends_user”
your user model should look like this:
class User “User” ,
:join_table => “friends_users” ,
:association_foreign_key => “friend_id” ,
:foreign_key => “user_id”,
:after_remove => :no_more_mr_nice_guy
def no_more_mr_nice_guy(friend)
friend.friends.delete(self) rescue nil
end
end
leave the model as it is for friends_users
Here is the friends_user controller:
class FriendsUserController ["user_id = ? AND friend_id = ?",userid,friendid])
#make sure the friendship is not self-friendship
if userid == friendid
flash[:notice] = ‘You are already your own friend!’
redirect_to :controller => ‘user’, :action => ’show’, :id => @session['user'].id
#and make sure there isn’t already a friendship pending from you
elsif friendshipuser
if friendshipuser.requestfromuser == 1
flash[:notice] = ‘There is already a friendship request pending’
redirect_to :controller => ‘user’, :action => ’show’, :id => @session['user'].id
#and make sure there is not already a current friendship
elsif friendshipuser.requestfromuser == 0 and friendshipuser.requestfromfriend == 0
flash[:notice] = ‘You are already friends!’
redirect_to :controller => ‘user’, :action => ’show’, :id => @session['user'].id
#make sure there isn’y already a friendship pending from them
elsif friendshipuser.requestfromfriend == 1
redirect_to :action => ‘acceptinvitation’, :friend_id => friendid
end
else
@friendid = @params['friend_id']
FriendsUser.create(
:user_id => @session['user'].id,
:friend_id => @friendid,
:requestfromuser => 1
)
FriendsUser.create(
:user_id => @friendid,
:friend_id => @session['user'].id,
:requestfromfriend => 1
)
flash[:notice] = ‘Requested ‘ + User.find(@friendid).firstname + ‘ to become your friend!’
redirect_to :controller => ‘user’, :action => ‘list’
end
end
def list
@user = @session['user']
@frienduserreq = FriendsUser.find(:all, :conditions => ["user_id=? AND requestfromfriend=0 AND requestfromuser=0",@user.id])
end
def acceptinvitation
userid = @session['user'].id
friend = User.find(@params['friend_id'])
@friendsuser = FriendsUser.find(:all, :conditions => ["(user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?)",userid,friend.id,friend.id,userid])
for cementrelationship in @friendsuser
cementrelationship.update_attributes(
:requestfromuser => 0,
:requestfromfriend => 0)
end
flash[:notice] = ‘You are now friends with ‘ + friend.firstname + ‘ ‘ + friend.lastname + ‘!’
redirect_to :controller => ‘user’, :action => ’show’, :id => @session['user'].id
end
def declineinvitation
unwantedfriend = User.find(@params['friend_id'])
@session['user'].friends.delete(unwantedfriend)
flash[:notice] = ‘You declined ‘ + unwantedfriend.firstname + ‘\’s request to become your friend’
redirect_to :controller => ‘user’, :action => ’show’, :id => @session['user'].id
end
def destroyfriendship
unwantedfriend = User.find(@params['friend_id'])
@session['user'].friends.delete(unwantedfriend)
flash[:notice] = ‘You are no longer friends with ‘ + unwantedfriend.firstname
redirect_to :controller => ‘user’, :action => ’show’, :id => @session['user'].id
end
end
Remember to substitute the redirect locations to wherever you want them to go
I also have a small method in the user controller:
def show
@friendsuser = FriendsUser.find(:all, :conditions => ["user_id=? AND requestfromfriend=0 AND requestfromuser =0",@params[:id]])
id = @params[:id].to_i
if id == @session['user'].id.to_i
@user = @session['user']
@frienduserreq = FriendsUser.find(:all, :conditions => ["user_id=? AND requestfromfriend=1",@user.id])
if @frienduserreq == []
@frienduserreq = nil
end
else
@user = User.find(@params[:id])
end
end
In the views for friends_user add a view called list.rhtml:
That’ll list all the names of your friends
This doesn’t work with the “Brian”)
p2 = Person.create!(:name => “Tony”)
p1.friends [#"Tony", "id"=>"2"}>]
p1.reload
p1.friends => []
The offending SQL that generates the faulty relationship is:
INSERT INTO friendships (`authorized`, `person_id`, `friend_id`) VALUES(0, 2, NULL)
It seems to be setting ‘person_id’ to the value that should be assigned to ‘friend_id’ and then setting ‘friend_id’ to NULL
What gives?
[...] has_many :through Self-referential Example û Aldenta: Web Nourishment How to create a self-referential relationship in Rails–good code for a FOAF application. (tags: rails hmt howto database joins active-record foaf) [...]
Hi,
I sort of have this working, but am a little confused on whether it should do what I really want.
My table is users rather than persons.
I access the friends by
@user.friends.each do etc
which gets the friends for that user_id in the friendships table.
but what if that user is in the friend_id?
Nick, it should work fine since @user.friends will make an SQL call that says “user_id = ?” making sure only friends associated with that user_id are found.
If you’ve befriended yourself then you might get back your own user object… which is your best friend… or something.
Nice example. Quick question:
# create the has_many :through relationship
has_many :friends, :through => :friendships
# an example of how to get only the authorized friends
has_many :authorized_friends, :through => :friendships,
:source => :friend, :conditions => [ "authorized = ?", true ]
Why do you need :source in :authorized_friends but not in :frinds?
Rails won’t be able to tell what table to use in that case without giving the source. Here’s what the documentation has to say about has_many source:
Just wanted to thank you for this post… it really helped me out today.
When removing a friend, would you have to manually remove the friendship as well as the friend?
current_user.friends.delete(unwanted_friend)
and
unwanted_relationship = current_user.relationships.find_by_friend_id…
current_user.relationships.delete(unwanted_relationship)
art, Are you using destroy or delete? Remember, in rails that delete just deletes the record from the database but destroy will call the correct callbacks to destroy associated objects.
this was helpful. particularly the comment in the example source:
# tell ActiveRecord that a person has_many friendships or :through won’t work
thanks!