has_many :through Self-referential Example

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

23 Responses

  1. 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.

  2. Thanks so much. Always nice to hear when an example helps someone.

  3. Many thanks for your great example!

  4. Glad it helped you Jason!

  5. 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
      end
    

    Although 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

  6. 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:

    friendship = Friendship.new
    friendship.person_id = @current_person.id
    friendship.friend_id = newFriend.id
    friendship.save!
    

    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?

  7. 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?

  8. 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.

  9. 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.

  10. 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

  11. 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?

  12. 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?

  13. 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.

  14. 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?

  15. 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:

    :source: Specifies the source association name used by has_many :through queries. Only use it if the name cannot be inferred from the association. has_many :subscribers, :through => :subscriptions will look for either :subscribers or :subscriber on Subscription, unless a :source is given.

  16. Just wanted to thank you for this post… it really helped me out today.

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

  18. 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.

  19. this was helpful. particularly the comment in the example source:

    # tell ActiveRecord that a person has_many friendships or :through won’t work

    thanks!

  20. You made my day John.
    :)

    And also very well commented.

    Thanks

  21. Very nice article.

    Anyone have any suggestions getting this to work when both the friendships table and the persons table are using STI?

    I have been trying but keep on getting AsociationTypeMismatch errors.

  22. Hi ,
    I wanted to know how i can set “authorized” field to true.. Actually what I am doing is, current user can add another user as friend(which will send him a friendship request).
    Now, when he clicks on Add user or allow user I want to set authorized to true. But since there is no id field in the table, I can neither say update_attribute nor can i call save! method.
    Thanks for any help
    Sunil