# Self-Referential, Many-to-Many Relationships

> Modeling a friends list in Rails using has_and_belongs_to_many with a self-referencing join table

- Author: Curtis Miller
- Published: 2006-10-18
- Categories: rails, how-to
- Canonical URL: https://millarian.com/posts/self-referential-many-to-many-relationships/

Wow, that title is a mouthful! Yet, it refers to a concept that I explored today in my [Ruby on Rails](http://www.rubyonrails.com) studies.

So, just a quick background on the topic. A self-referential relationship refers to a relationship between a class and itself. For example, every social networking website has the concept of a User (Member, Person, whatever). Now, it wouldn't be very social if those users could not have friends, so Users are allowed to add Friends. But what is a Friend, really? A Friend is just another User on the site. Therefore, a User has a relationship to another User (or many Users).

In the Ruby on Rails framework we need to change this slightly to allow us to use the built in semantics that is offered for traversing a relationship. See [belongs\_to](http://wiki.rubyonrails.com/rails/pages/belongs_to), [has\_one](http://wiki.rubyonrails.org/rails/pages/has_one), [has\_many](http://wiki.rubyonrails.org/rails/pages/has_many), and [has\_and\_belongs\_to\_many](http://wiki.rubyonrails.com/rails/pages/has_and_belongs_to_many). In Ruby on Rails we add a table to join the User with his/her Friends (i.e., other Users).

So, how do we make use of this? In your Users model, add the following:

```ruby
class User < ActiveRecord::Base
  has_and_belongs_to_many :friends,
                          :class_name => "User",
                          :join_table => "users_friends",
                          :foreign_key => "user_id",
                          :association_foreign_key => "friend_id"
end
```

This creates the relationship between the User and other Users. The symbol `:friends` describes how this relationship should be referred to. That is, `user.friends`. The `:join_table` option specifies the route taken to get to the class specified by `:class_name`. Lastly, `:foreign_key` and `:association_foreign_key` are the two keys that will be used during the traversal. You can also add `:after_add` or `:after_remove` options specifying a method that should be called after an add or remove respectively. For example, you may want the friendship to be bi-directional, meaning if I add you as a friend, then you add me as a friend automatically.

Now we can do something like add a new method to add a friend from within the User model.

```ruby
def add_friend(friend)
  self.friends << friend unless self.friends.include?(friend) || friend == self
end
```

Then just invoke this method when we receive an action that involves adding a friend.

So, what if you wanted to add another attribute to your join table? Say we want to record the date and time that the two Users became friends. We add an column called `friends_since` to the users\_friends table. What do we need to modify in the User model file? Not much it turns out, the `add_friend` method becomes:

```ruby
def add_friend(friend)
  self.friends.push_with_attributes(friend, :friends_since => Time.now()) unless self.friends.include?(friend) || friend == self
end
```

Slightly more complicated, but not by much.

So, what about updating the join table? For example, in some social networking sites it is not allowed to create a bi-directional relationship automatically. The User on the other end must accept the originating User's request to be friends. Even if I consider you my friend, you may not consider me as your friend. C'est la vie…

We should add an attribute that indicates acceptance of the friendship, otherwise it is in a pending state. We add `accepted` to our users\_friends join table. It's okay for the requester to accept automatically, so we just need to add that to the code above. Pretty trivial. Except, now we need to have some way to update the join table when the friend User accepts the request. How do we do that?

This is where I am a little fuzzy, but here is what I tried:

```ruby
def accept_friend(friend)
  sql = User.sanitize(["UPDATE users_friends SET friends_since = ?, accepted = ? WHERE member_id = ? AND friend_id = ?", Time.now(), 1, self.id, friend].flatten)
  self.connection.update(sql, "Accept Friend")
end
```

Doesn't exactly look elegant, so if you know of another way to accomplish this, please let me know.
