#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 45 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. JohnMerlino Jan 15, 2010 at 14:26

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


36. JohnMerlino Jan 15, 2010 at 14:31

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


37. iPod/iPhone Jailbreak Apr 08, 2010 at 18:10

Thanks ryan, great video. Learned a lot :)


38. Neal Apr 12, 2010 at 07:40

That's awesome, exactly what I needed! Thanks Mr. B!


39. iPod Dude Apr 13, 2010 at 14:26

Oh, what if a user is already a friend, and I dont want to display the "Add Friend" link next to their name? How can I check if they are already friends, and only display "Add Friend" if they are not?

THANKS!


40. evo10 Apr 15, 2010 at 11:25

great rails cast. is delete automatically understood as the destroy action? and the post method will also be understood as create action? how is that possible? super noob here or should i just trust that that works?


41. Gang May 01, 2010 at 06:34

very nice. thanks author.


42. varun May 06, 2010 at 07:36

great video, thank you.


43. Lee Hans Jun 05, 2010 at 22:05

How do you prevent a user from befriending himself? For example, RyanB can friend RyanB in your video.


44. Lee Hans Jun 05, 2010 at 23:38

Never mind, worked out how to do this with a named_scope that takes the current record as a parameter to a lambda expression. This is then passed into the conditions expression to exclude the specified record.

Thanks for a great screencast!


45. Ball Bearing Jun 20, 2010 at 23:02

All kinds of Ball Bearing, Ball Bearings, Deep Groove Ball Bearing, Housing Bearing,Miniature bearings, Automobile Bearing, Electric Tool Bearings, Textile Machine Bearing, Computer Series Bearing and so on."


46. Kendall Jun 29, 2010 at 05:20

Thanks Ryan. You set the bar.


47. chanel boots sale Jul 23, 2010 at 01:41

they are very comfortable to wear.


48. timberlandbootsuk Aug 02, 2010 at 02:01

we provide our buyers with an efficient and manageable procurement process covering every phase of the international supply chain and

streamlining trade channels. Also welcome wholesaling, feedback now!


49. Coach Outlet Aug 12, 2010 at 03:42

Here we have new style Coach Handbags.All the Coach purses are good quality and lower price.A fashion Coach Outlet is dreamed by the fashion females.Welcome to our store discountbagshop.com.I am sure you will find one for yourself.


50. p90 workout Aug 12, 2010 at 09:13

Thanks for writing this. I really feel as though I know so much more about this than I did before. Your blog really brought some things to light that I never would have thought about before reading it. You should continue this, Im sure most people would agree youve got a gift.


51. Air Jordan AJF 3 Aug 14, 2010 at 01:09

Deep Groove Ball Bearing, Housing Bearing,Miniature bearings, Automobile Bearing,Here we have new style Coach Handbags.All the Coach purses are good quality and lower price.A fashion Coach Outlet is dreamed by the fashion females.


52. supra tk society Aug 18, 2010 at 18:47

good job,good article


53. Air Jordan Force 4 Aug 20, 2010 at 01:40

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.


54. wholesale new era caps Aug 20, 2010 at 20:33

The blog article very surprised to me! Your writing is good. In this I learned a lot! Thank you!


55. Wholesale Electronics Aug 25, 2010 at 01:58

Discount Wholesale Electronics, Wholesale Cell Phones, Electronic Gadgets and More from the Best Dropship Wholesaler


56. cool stuff Aug 25, 2010 at 02:15

very cool article ,like my cool stuff .very useful.thanks for sharing the article!


57. louis vuitton shoes Aug 26, 2010 at 20:51

Thanks for sharing your article. I really enjoyed it. I put a link to my site to here so other people can read it. My readers have about the same interets


58. wholesale Cell Phones Aug 30, 2010 at 20:11

thanks for your sharing, it is really useful.


59. snow boots Aug 30, 2010 at 21:00

How can I check if they are already friends, and only display "Add Friend" if they are not?


60. Cheap Supra Shoes Aug 31, 2010 at 23:32

Thanks very much for publishing so perfect article.


61. GHD Hair Straighteners Aug 31, 2010 at 23:33

Perfect article


62. GHD Australia Aug 31, 2010 at 23:34

Great


63. louis vuitton sunglasses Sep 01, 2010 at 21:28

Intimately, the post is actually the best on this laudable topic. I harmonize with your conclusions and will eagerly look forward to your future updates. Saying thanks will not just be adequate, for the fantastic lucidity in your writing.

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