#163 Self-Referential Association
May 25, 2009 | 14 minutes | Active Record, Controllers
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:
- source code
- mp4
- m4v
- webm
- ogv
Great job bro!!
always wait for your casts...
Very nice - I wasn't aware of the inverse parameter
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:)
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?
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?
*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?:)
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.
@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?
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?
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 :-)
This is exactly what I needed!
Thank you, Ryan.
Ryan, have you tried the acts_as_network plugin?
Hey, acts_as_network looks sweet! Thanks
you must have been hungry.. you typed "dessert" instead of delete.. :-D
@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.
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
Thanks again Ryan. Keep up the good work. I would love to see a Globalize 2 and Rails 2.2 tutorial.
@Jason
You can specify the foreign_key to use:
class Reseller < Contact
has_many :contacts, :through => supervisions, :source => :user, :foreign_key => 'user_id'
end
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
@zzg: Nifty scaffold is a part of Ryan's Nifty Generators:
http://github.com/ryanb/nifty-generators/
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
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?
“Your comment at the beginning about where to put controller code was especially valuable.”
— 24. Brian Armstrong
Yeap Ryan Bates is my hero
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 !!
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...
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'
Another brilliant episode, Ryan. Keep up the great work!
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.
Did you ever figure how to delete the reciprocal relationship? Seemed like an obvious omission in this otherwise excellent episode.
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
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 :)
current_user getting error in this.
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...
What if you don't use the plugin? What will the value of current_user be?
I also don't understand why we had to define user here: (:friend_id => user)
That's awesome, exactly what I needed! Thanks Mr. B!
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?
great video, thank you.
How do you prevent a user from befriending himself? For example, RyanB can friend RyanB in your video.
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!
Thanks Ryan. You set the bar.
how do find mutual friends as well as indifferent people?
https://gist.github.com/968648
this is my attempt as of now. i'm thinking of using a collect maybe to loop through one set first but there but be a better option.
Your Railscasts are definitely one big reason for developing in Rails. Thanks
If you add a status boolean in the friendship table you can do away with the invert_friendship assosiation
Hey Ryan, I was trying this with rails 3.1.3
and it says to set source =>
can you help how we can control join table on condition
any guess how i can change this to
Thanks :-)
Helped me overcome a seemingly difficult situation. The inverse_relationship helped a lot.
Thanks!
Would it be better if before running the migration you add an index for friend_id on the friendships table?
Ryan, thanks very much! I use Railscasts every day, finding answers to any questions. it is programmers' desk book indeed!
this is great screencast, I am trying to do something similar, however, when I add friend, I want it automatically add the other side of the friendship too, something like in facebook. any ideas what I should be adding to my friendship create action.? thanks!
Hey Ryan,
thanks for this great screencast.
I have only one problem:
Is there any way to write a scope or maybe a query with arel to get all friends, own and inverse, in one query?
I have the same question. Did you ever figure out a way to do this?
Great screenscast.
I'm trying to adapt it to a blog app where the posts have a "related posts" feature.
So one post has many related_posts trough related_posts_association.
While the implementation works out of the box and I can create relationships in the console I can't find a way to make it work in the application.
In this railscast example the create action uses the "current_user" that is stored in a session by the auth system. In my example I would like to create the relationship in the _form view (new and edit) where I create a list of all the other posts, each one next to a checkbox.
Once the create or update button is pressed I would like to have also the relationships created.
Does anyone have an idea on how to implement that?
Is there a good way to prevent an user from adding (following) a friend more than once?
EDIT: This will do it:
Instead of listing users one by one can't use dropdown list ?
Ryan, you could provide the source code before and after the screencast... that would be amazing...
the first source code, so we can follow you
the second source code, so we can check the app actually working
:)
I came up with this solution to bidirectional with methods for:
* mutual - relationship in both directions
* pending - relationship only forward
* requesting - relationship only backward
Any suggestions for improvement?
I would not mind seeing a bit more how you got this working. Looks interesting
I forked new one with Rails 3.2.21 and Ruby 1.9.3
git clone https://github.com/abelard2008/163-self-referential-association.git
Hi Ryan,
I followed this tutorial through but when i got to displaying users in my view, it worked for a bit and then it suddenly started throwing me this error:
undefined method `profile' for #Friendship:0x007fd8519f11f0
and im unsure where it came from. I applied friendly_id gem to my users after this point as far as my memory takes me so im not sure if that influences this in anyway. My models and controllers match to yours too so im sure that the code on that side isnt the problem.
Any ideas? Thanks
Nandu.
I have the exact same problem as you have NanduShah. Did you manage to fix it in your case? I still can´t figure out what´s wrong with my code.
All the best
Dadi
This episode code is upgraded to Rails 5, check out the source code fb5
I am finding it hard to get this working. It looks to me as if my code is exactly the same apart from naming.
User model
representative grouping model
I didnt use a migration to make this table as I already had it, i just renamed the name table name to suit
i test it with code like this:
and try to view it like so:
but i get this error:
if i just print out the 'group':
and i can access its values, but it fails when i try to access the 'representative'.
I feel that its probably pretty close and that ive messed up a naming convention somewhere. Can anyone spot where the issue is?