#163
May 25, 2009

Self-Referential Association

Creating a social networking site often requires a self-referential association on the User model to define friends/followers. In this episode I show how to do exactly that.
Download (22.9 MB, 14:36)
alternative download for iPod & Apple TV (17.4 MB, 14:36)

Resources

script/generate nifty_scaffold friendship user_id:integer friend_id:integer create destroy
rake db:migrate
# models/user.rb
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

# friendships_controller.rb
def create
  @friendship = current_user.friendships.build(:friend_id => params[:friend_id])
  if @friendship.save
    flash[:notice] = "Added friend."
    redirect_to root_url
  else
    flash[:error] = "Unable to add friend."
    redirect_to root_url
  end
end

def destroy
  @friendship = current_user.friendships.find(params[:id])
  @friendship.destroy
  flash[:notice] = "Removed friendship."
  redirect_to current_user
end
<!-- users/index.html.erb -->
<%= link_to "Add Friend", friendships_path(:friend_id => user), :method => :post %>

<!-- users/show.html.erb -->
<h2>Friends</h2>
<ul>
  <% for friendship in @user.friendships %>
    <li>
      <%=h friendship.friend.username %>
      (<%= link_to "remove", friendship, :method => :delete %>)
    </li>
  <% end %>
</ul>

<p><%= link_to "Find Friends", users_path %></p>

<h2>Friended by Users</h2>
<ul>
  <% for user in @user.inverse_friends %>
    <li><%=h user.username %></li>
  <% end %>
</ul>

RSS Feed for Episode Comments 40 comments

1. xinuc May 25, 2009 at 00:11

Great job bro!!

always wait for your casts...


2. Adam Hill May 25, 2009 at 01:14

Very nice - I wasn't aware of the inverse parameter


3. Fredd May 25, 2009 at 01:39

Thanks for this review of many to many associations and much more! It's funny because I discovered a lot of these features my self when implemented a group based permission system last week. Groups had memberships to different sections and so one (almost the same thing). The problem I discovered though was that it's hard to sort (in my case) the groups belonging to a certain section because you looping over the membership. If I wanted to sort by name it wouldn't work because the name field is not in the membership table. It's probably easy to fix this, I just need to read the API some more:)


4. NeilCauldwell May 25, 2009 at 02:30

I could have done with this 18 months ago; I spent yonks working through the complexities of a 'I follow you, but you might not follow me' association type with the people on the RailsSpace google group.

The app ended up with a Subcriber/Subscribee naming convention (it sums up how people are interacting with one another on social networks nowadays), and, therefore, used a subscribers_controller and subscribee_controller, giving /users/1/subscribers and /users/1/subscribees routes and API. Is that something you looked at for this Friendships example, Ryan?


5. Peter D May 25, 2009 at 02:41

Great screencast as always! I wasn't aware of the inverse parameter yet either.

I am missing one thing though; I want someone who was "friended by" to be able to cancel the friendship and not just leave it up to the one who initiated the friendship.

Also I wonder won't this method leave you open for duplicate friendships? After Ryan befriends Fred, Fred can still befriend Ryan as well. Is there a way to validate against this without custom logic?


6. Fredd May 25, 2009 at 02:59

*Adam Hill

I don't think theres a special inverse parameter. Ryan just creates another association with some special conditions and because the name will interfer with the first "friends" association he add the namespace "inverse" to it. Or am I wrong?:)


7. Philippe Lafoucrière May 25, 2009 at 04:38

Very nice !
Anyway, you don't get all friends at once like this. I've exposed the problem here : http://stackoverflow.com/questions/788582/cant-define-joins-conditions-in-hasmany-relationship

For performances reasons, friendships must be retreived both ways in one request, especially if your friendship table contains 1.000.000+ lines like in http://woopets.com !
 It's apparently not doable with rails 2.3.


8. Jason May 25, 2009 at 05:41

@Ryan: great screencast, this comes just right in time. Could anyone advise on how to adapt this with STI user models, e.g.

class User < ARB
class Contact < User
class Reseller < Contact
class Employee < Reseller

Both, resellers and employees should have many contacts through supervisions (:user_id => references, :contact_id => integer). If i use:

class Reseller < Contact
  has_many :contacts, :through => supervisions, :source => :user
end

ActiveRecord throws an InvalidStatement error, because there is no reseller_id column in the supervisions table. Any hints?


9. pulkit May 25, 2009 at 05:44

Hi Ryan,
Thanks for screencast.

One thing I noticed that in any social networking site you can't add any person directly!!!

You must need permission of that person.

Did you miss it or you didn't include it because of lack of time?


10. Murdoch May 25, 2009 at 09:08

I further recommend this article about how to get through this jungle of redundant ? Friendship, Membership, XYZOwnership & Co. assocation tables to put/abstract them into 1 table (acts_as_double_polymorphic_join) with the yet up to date plugin () has_many_polymorphs:

http://m.onkey.org/2007/8/14/excuse-me-wtf-is-polymorphs

Correct me for a niftier solution :-)


11. Daniel Mazza May 25, 2009 at 09:29

This is exactly what I needed!
Thank you, Ryan.


12. Mauro Tortonesi May 25, 2009 at 09:48

Ryan, have you tried the acts_as_network plugin?


13. iGEL May 25, 2009 at 12:29

Hey, acts_as_network looks sweet! Thanks


14. T May 25, 2009 at 12:56

you must have been hungry.. you typed "dessert" instead of delete.. :-D


15. Adam Hill May 25, 2009 at 17:47

@Fredd - yes you're right. That's what I get for commenting before I've finished the s'cast. I'm used to the auto-recipricate type self-referential many-to-many as per the excellent Rails Recipes book and based on the notes thought it might perform those functions automatically.


16. Bharat Ruparel May 25, 2009 at 18:19

Hello Ryan,
The last two episodes have been especially useful. I am still waiting for your nested forms and more jquery episodes either here or as screencasts at PragProg. The ActiveRecord and Forms screencasts have been extremely useful too.
Thanks.
Bharat


17. zzg May 25, 2009 at 19:30

nifty_scaffold 找不到!


18. George May 25, 2009 at 20:29

Thanks again Ryan. Keep up the good work. I would love to see a Globalize 2 and Rails 2.2 tutorial.


19. Matt Beedle May 26, 2009 at 01:23

@Jason

You can specify the foreign_key to use:

class Reseller < Contact
  has_many :contacts, :through => supervisions, :source => :user, :foreign_key => 'user_id'
end


20. Peter D May 26, 2009 at 13:08

For anyone struggling, like me, to get a good implementation of this without having to resort to calling two different associations in your views/controllers, there's an awesome plugin called "has_many_friends" on github that handles all the annoying details for you.

http://github.com/swemoney/has_many_friends/tree/master


21. Peter D May 26, 2009 at 13:10

@zzg: Nifty scaffold is a part of Ryan's Nifty Generators:

http://github.com/ryanb/nifty-generators/


22. Brian Armstrong May 27, 2009 at 10:40

Wow Ryan...sometimes I feel like I learned more here than in 4 years of computer science classes.

Your comment at the beginning about where to put controller code was especially valuable. My users controller is littered with tons of methods like that.

I don´t know how you keep making these with little or no compensation...we need to find a business model for you so that you can continue producing these...you have a gift!
Brian


23. David May 27, 2009 at 21:31

I really enjoyed this episode, and it's falling right in line with a project I'm working on.
One of the questions I have about this set up though is this: how can we set it up so that if one member of the friendship destroys the friendship, the whole thing goes and not just half?


24. Minikin May 28, 2009 at 04:15

Big Up!


25. Memiux May 28, 2009 at 10:07

“Your comment at the beginning about where to put controller code was especially valuable.”
— 24. Brian Armstrong

Yeap Ryan Bates is my hero


26. Nadav May 28, 2009 at 13:02

Thanks. Great episode :-)
Two quick questions:
a) Maybe I'm missing something: Where was the 'current_user' variable set ?
b) Didn't the addition of he "remove" option the the users/show view introduced the "n+1" problem? It seems that a separate query has to be issued for each friendship in order to get the friend's name. Am I right ? Should there be an 'include' clause somewhere in order to fix it ?

Thanks so much for your casts !!


27. Squiddhartha May 28, 2009 at 14:39

Nadav, the 'current_user' variable is part of the authentication plugin Ryan is using, which looked like either restful_authentication or his own nifty_authentication...


28. Jay May 29, 2009 at 13:36

I get the following when I try to add a friendship:

 ArgumentError in FriendshipsController#create

Unknown key(s): Friendship

RAILS_ROOT: /Users/Jay/Ruby/bull
Application Trace | Framework Trace | Full Trace

/Library/Ruby/Gems/1.8/gems/activesupport-2.3.2/lib/active_support/core_ext/hash/keys.rb:47:in `assert_valid_keys'
/Library/Ruby/Gems/1.8/gems/activerecord-2.3.2/lib/active_record/associations.rb:1506:in `create_belongs_to_reflection'
/Library/Ruby/Gems/1.8/gems/activerecord-2.3.2/lib/active_record/associations.rb:994:in `belongs_to'
(eval):3:in `belongs_to'


29. David Beckwith May 31, 2009 at 20:13

Another brilliant episode, Ryan. Keep up the great work!


30. Wolfram Arnold Jun 05, 2009 at 01:43

Nice exhibit, Ryan. My question is how you handle the delete of the respective reciprocal friendship? I've found that to be a case where the :after_remove option on the has_many has failed me, and I had to resort to some kludges. I wonder if there is a clean(er) way? You don't cover that part.


31. Dean R Jul 14, 2009 at 20:33

Thank you Ryan, for another concise and very accurate one. I'm glad you did this the 'right' way through a join model. Self-referential made me think of this: http://pastie.org/546374, ugh !

This article was a better example of design than it was of 'railsy' stuff. Very hip and MDD ;)

Nice way to redeem yourself for the one where a rails controller spawns of a background process by appending an ampersand to a process name. You kick of processes per request, and you may as well be doing CGI. I know you may have a legitimate purpose and that you do not do this randomly. But also because it strikes me as a poorly documented dependency, one which even at runtime exists only ephemerally.

Anyway, nice one this time. Also, do you know of Dr. Nic - for some more design-level stuff you could show http://drnicwilliams.com/2006/10/04/i-love-map-by-pluralisation/ or metamagic models. I think his stuff is cool..

Ciao


32. Dean R Jul 14, 2009 at 20:43

Also - how you manage to do this with only Saturday afternoons and Sundays not even that late into the night.. http://github.com/ryanb/railscasts-episodes/graphs/punch_card Maybe you could post an episode on how to balance the demands of life and code, cause you seem superorganized. http://github.com/chicagogrooves/chess_on_rails/graphs/punch_card bears so little resemblance to yours its hilarious :)


33. ashish Aug 13, 2009 at 06:01

current_user getting error in this.


34. Emerson Oct 01, 2009 at 15:35

Trying to implement this sort of functionality into my site, but having some small problems.

1. I'm using the restful authentication plugin

2. Each "user" has_one "Profile".

Here is my question & problem:

1. Should I associate the friendship with the Profile_id or the User_id? Is one more efficient than the other?

2. How can I get the Profile_id into the create method of the friendships_controller? (if that's what I end up using)

PS - Love your screencasts consistently blown away...


35. UGGS Online Dec 27, 2009 at 18:58

Great job bro!!


36. buy discount watches here Jan 06, 2010 at 19:02

thank you


37. JohnMerlino Jan 15, 2010 at 14:26

What if you don't use the plugin? What will the value of current_user be?


38. JohnMerlino Jan 15, 2010 at 14:31

I also don't understand why we had to define user here: (:friend_id => user)


39. cheap adidas shoes Jan 31, 2010 at 19:00

Adidas Shoes Online Shop-Hot Selling Adidas Shoes & Cheap Adidas Shoes


40. Louis Vuitton handbags Mar 09, 2010 at 19:14

railscasts.com/episodes/163-self-referential-association

Add your comment:

(SKIP THIS ONE)

(required)

(not shown)


(use pastie or gist for code)

sponsored by:
if you want to help:
required:
Get Quicktime Player
Give Back to Open Source