#163 Self-Referential Association
Below is a page from a basic social networking application. A user can sign up, then log in and interact with other users. The page shown displays a list of the users and has a link next to each one that allows you to add them as a friend.
Although the link is there, it currently does nothing. In this episode we’ll write the code needed to enable a user to add other users as friends. To do this we’re going to need to create a self-referencing association: users will have relationships with other users, but instead of creating a relationship between two different models, we’ll be creating a relationship between two instances of the same model.
Generating The Right Actions
The “Add Friend” link on the users page doesn’t as yet link to anything.
<%= link_to "Add Friend" %>
At some point we’re going to have to wire it up to an action in a controller, and it’s worth thinking a little now about what controller and action it will call when it’s clicked. We already have a
UsersController, so this might seem the obvious place to deal with friends as friends are just other users. We could add two new actions to the
UsersController to deal with adding and removing friends.
def add_friend end def remove_friend end
This, however, is wrong for several reasons. The first alarm bell that should ring in your head when you start creating controller methods like these is that they are not among the seven standard RESTful methods (index, show, new, create, edit, update and destroy). Another concern should be raised by the fact that these methods both have
_friend appended to them. This suggests that we’re putting these methods in their own namespace. The final clue that this isn’t the best approach is the first part of each method name:
remove indicate that we’re creating and destroying a resource. All of these points give us a big hint that we should be creating a new controller for handling friends.
The controller we’ll create will be called
FriendshipsController. The naming of this controller is important. We might have called it
FriendsController but in our application a friend is just another
User, and we’ll be using our new controller to create and destroy relationships between users, not to create and destroy users.
A user can have many friends and be befriended by many other users so we’re going to have to create a many-to-many relationship. There are two ways to define this type of relationship in Rails:
has_many :through. (You can find more information about these two methods by watching Railscast 47). These days,
has_many :through is the most used, and it’s what we’ll be using to define our friendships.
has_many :through we’ll need to create a join model, which we’ll call
Friendship. This model will have two fields, a
user_id that represents the current user who’s adding a friend and a
friend_id that represents the user who’s being befriended.
We’ll create our new model in the usual way,
script/generate model Friendship user_id:integer friend_id:integer
and then run the generated migration.
Along with the model we’ll need to generate the
FriendshipsController we mentioned earlier.
script/generate controller Friendships
As we’re using
Friendship as a resource we’ll need to add the following line to
Now that we have our model and controller we can start to define how a
Friendship works. We’ll start off by defining the relationships between
Friendship in the model classes.
class Friendship < ActiveRecord::Base belongs_to :user belongs_to :friend, :class_name => 'User' end
User, who will be the user who initialised the relationship, and will also belong to a
Friend, who is the user who has been befriended. For the
friend relationship we need to explicitly specify the class name as ActiveRecord cannot work out the model to use from the name in the association.
We’ll also need to define the other half of the relationship in the
class User < ActiveRecord::Base has_many :friendships has_many :friends, :through => :friendships # rest of class omitted. end
The relationship here is defined as you would define any other many-to-many relationship. There’s no need to specify any custom naming as the relationships can all be worked out from the names of the relationships.
Wiring Up The “Add Friend” Link
Now that the relationships between the models have been defined, and we have our
FriendshipsController we can start work on the “Add Friend” link in the users’
<% for user in @users %> <div class="user"> <p> <strong><%=h user.username %></strong> <%= link_to "Add Friend", friendships_path(:friend_id => user), :method => :post %> <div class="clear"></div> </p> </div> <% end %>
The link needs to call the
create method in the
FriendshipsController. To do that we have the link POST to the
friendships_path. That isn’t quite enough though; we also need to pass the user that we’re going to befriend. We do that by adding it as a parameter to the
friendships_path method. That will pass the
id of the user to the
create action so that it knows which user we’re befriending.
Now that the link code is written we’ll move on to the
FriendshipsController and write the
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[:notice] = "Unable to add friend." redirect_to root_url end end
We create the new friendship by taking our current user and building a friendship through it, passing it the
id of the user we’re befriending. Using
build will mean that the
user_id of the new
Friendship is automatically set, and as we’ve passed the
friend_id from the parameters the relationship is fully defined. We can then save the relationship and redirect back to the home page, displaying a
flash notice that says that the friend has been added. If for some reason the relationship is invalid then we’ll show a different
flash notice. Obviously, if this was an application that was going into production then we’d give the user more specific information as to why their request failed.
Viewing Our Friends
A logged-in user can now click the “Add Friend” link and make another user their friend. They can’t, however, see a list of their friends. To fix this we’ll modify the user’s profile page so that they can see who they have added as a friend. The profile page is the
show page for a
User. Currently it just shows their name and provides a link that allows them to find friends.
<% title "My Profile" %> <p>Username: <%=h @user.username %></p> <p><%= link_to "Find Friends", users_path %></p>
To display a list of their friends, we can loop through the user’s
friends collection and display each one.
<h2>Your Friends</h2> <ul> <% for user in @user.friends %> <li><%= h user.username %></li> <% end %> </ul>
The profile page will now show the list of our friends.
It would be useful if there was a link next to each of our listed friends that allowed us to remove that user as a friend. To implement that we need to add a link to the
destroy action, passing the
id of the friendship. It’s here where things could get a little tricky as we’re looping through
Users and don’t have access to the
Friendship model to get the
id that identifies the relationship. This is a common problem that beginners to Rails encounter when they use many-to-many relationships as they can forget that there is a join model that we can interact with directly rather than, in this case jumping directly from
Instead of looping through a user’s friends to create the list, we’ll instead loop through their friendships. The code in the view will now change to this:
<h2>Your Friends</h2> <ul> <% for friendship in @user.friendships %> <li> <%= h friendship.friend.username %> (<%= link_to "remove", friendship, :method => :delete %>) </li> <% end %> </ul>
We can now get each friend’s name by using
friendship.friend.username and create our delete link by passing the friendship as a parameter to the link and telling it to use DELETE as the method so that it will call the
Talking of the
destroy action, it’s now time to implement it in the
def destroy @friendship = current_user.friendships.find(params[:id]) @friendship.destroy flash[:notice] = "Removed friendship." redirect_to current_user end
Note that in the first line of the action we search for the friendship only within the current user’s friendships. If we just called
Friendship.find(params[:id]) then a maliciously minded user could destroy relationships between any two users, when they should be restricted to only destroying relationships they themselves have made. The rest of the action destroys the friendship and then redirects back to the user’s profile page.
The profile page will now show a link and we’ll be able to remove a friendship with another user by clicking the link next to their name.
When creating self-referential relationships it’s important to remember that we’re only creating one side of the relationship. Although
paul listed as a friend above, if we were to visit
paul’s profile we wouldn’t see
eifion listed unless
paul had chosen to add him. We need two
Friendship records to create a mutual friendship.
To finish this episode we’ll add a list to the profile page so that a user can see who has them listed as a friend so that we can see the other side of this relationship. To do this we need to add two more relationships to the
class User < ActiveRecord::Base 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 #rest of class omitted. end
It’s difficult to think up appropriate names to define the other side of the relationship so we’ll prefix both with the word “inverse” to give us
inverse_friends. We need to specify some additional options to make the relationships work. For
inverse_friendships we’ll have to specify the name of the other model as it can’t be inferred from the relationship name and we’ll also have to define the foreign key as
friend_id. For the
inverse_friends relationship we need to specify the
users, as again it cannot be inferred from the name of the relationship.
Back in the profile page we want to show a list of the users who have befriended us. To do this we just need to loop through our
inverse_friends and display their user names.
<h2>Users Who Have Befriended You</h2> <ul> <% for user in @user.inverse_friends %> <li><%= h user.username %></li> <% end %> </ul>
If we log in as
paul and add
eifion as one of his friends, then log back in as
eifion we’ll now see
paul listed as having added
eifion as a friend.
That’s it for this episode. We haven’t quite recreated Facebook, but hopefully you’ve now got a good idea of how self-referential relationships work in Ruby on Rails.