dnite’s ‘old’ Blog

For my new blog, head over to http://blog.dnite.org

  • Moved Out!

    Thanks for stopping by my blog! It's been fun over here at Wordpress.com but I've moved to bigger, better things. My new blog is located here! Hope to see you there!

Ruby on Rails :: Restful friends with has_many :through

Posted by dnite on November 25, 2006

EDIT (2007-03-07) :: I’ve updated this as a plugin and released it at my new blog (http://blog.dnite.org). I’ll keep this information up here, but everything is a lot cleaner and works a lot better with the new plugin. So go check out my new blog!

EDIT (2007-01-23) :: I’m depreciating this entry/tutorial!! If you have problems getting this working, just be patient, I’m going to be releasing a plugin in the very near future that will work a lot better and be a lot easier to impliment than this! Just give me a few days…I’ve been working on a project of mine and while I learn Rails. One thing that surprised me was that as many tutorials there are on how to create a blog, there are very few about creating a friends system. So I’ll try and go through the system I put together today after many headaches (that I’ll explain later).

We’re going to keep it RESTful here (or as RESTful as we can). I’ve found that the easiest way to approach anything RESTfully is to understand the path first. So let’s start there. These are the path’s we’ll need to accomplish our friends methods.

# List a user's friends (index action, GET method)
/users/1/friends

# Confirm that a user wants said friend. (confirm action, GET method)
/users/1/friends/2;confirm

# Add a friend (add action, POST method)
/users/1/friends/2;add

# Remove a friend (destroy action, DELETE method)
/users/1/friends/2

# Show a friend
/users/1/friends/2 (show action, GET method)

We’ll need a couple of models, a couple of controllers, and a couple of routes. I’m assuming you know how to make a new Rails project, so let’s get started by creating the models. We’ll need a User model and a Friendship model. I’ll keep them simple here.

First, here’s the tables I created.

create_table :users do |t|
t.column :name, :string
t.column :created_at, :datetime
end

create_table :friendships, :id => false do |t|
t.column :user_id, :integer, :null => false
t.column :friend_id, :integer, :null => false
t.column :status, :integer, :null => false, :default => 0
t.column :created_at, :datetime
end

The models look like this.

class User < ActiveRecord::Base
has_many :friendships
has_many :friends, :through => :friendships
end
class Friendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, :class_name => 'User', :foreign_key => 'friend_id'
end

Now, here’s where I was pulling my hair out. Because, in theory, that association looks ok to me. But if I try and do something like…

User.find(1).friends

I get a nice and very strange SQL error.

Mysql::Error: Column 'friend_id' cannot be null: INSERT INTO friendships (`status`, `user_id`, `created_at`, `friend_id`) VALUES(0, 2, '2006-11-25 01:06:46', NULL)

The strange part isn’t the error. It’s that, one, there is no friend_id (and I still have no idea why) and two, it’s putting what SHOULD be the friend_id into the user_id column in the table. I still have yet to figure out why this happened, so I just went ahead and made my own little work around which works just fine and in all technicality, will probably work better down the road when I have more attributes in the friendships model. But if anyone has any ideas on what could be wrong, I’d love to hear about it.

Anyways.. Back on topic. Now we have the relationships, we need to interact with them. Being RESTful says that we need to do something like /users/1/friends/2 with the correct method. So I created a friends controller that looks like this.

class FriendsController < ApplicationController
def confirm
@friend = User.find_by_id(params[:id])
@current_user = current_user
end
def add
if !friends_already?(params[:user_id], params[:id])
add_friend(params[:user_id], params[:id])
end
redirect_to users_path
end

def destroy
if friends_already?(params[:user_id], params[:id])
remove_friend(params[:user_id], params[:id])
end
redirect_to users_path
end

protected
def friends_already?(user_id, friend_id)
user = User.find(user_id)
friend = User.find(friend_id)
return true if user.friends.include?(friend) && friend.friends.include?(user)
false
end

def add_friend(user_id, friend_id)
Friendship.create({:user_id => user_id, :friend_id => friend_id})
Friendship.create({:user_id => friend_id, :friend_id => user_id})
end

def remove_friend(user_id, friend_id)
Friendship.delete_all "user_id = #{user_id} and friend_id = #{friend_id}"
Friendship.delete_all "user_id = #{friend_id} and friend_id = #{user_id}"
end
end

The confirm action (/users/1/friends/2;confirm) shows a dialog w/ a button that lets the user confirm that he wants to be friends with this person. The button posts to /users/1/friends/2;add. From here it’s pretty self explanitory. It looks really nice when you can call /users/1/friends to list all of someone’s friends. Any extra attributes needed can be added in the add_friend action. Now all you need to do is add the new cool restful routes to config/routes.rb

map.resources :users do |user|
user.resources :friends, :member => { :confirm => :get, :add => :post }
end

That should do ya. Sorry this isn’t a full out tutorial, but you should definately have a good idea on how to impliment a friends system pretty quickly now. Make changes. Do what you want. Let me know if I screwed anything up. I’m still learning this stuff, so what I have here could be completely stupid. Let me know what you think.

About these ads

9 Responses to “Ruby on Rails :: Restful friends with has_many :through”

  1. [...] After having a hard time finding information on putting together a nice self-referential association for a friends list, the author has put together a guide to help out.read more | digg story Share and Enjoy:These icons link to social bookmarking sites where readers can share and discover new web pages. [...]

  2. Evan Farrar said

    I have no idea what this means. Typo?

    class User :friendships

    As for your sql error, your model-defined relations seem a little hard to follow. Maybe you need something like this?

    class User
    has_many :friends, :through => 'friendships'
    end

    class Friendship
    has_one :user
    has_one :friend, :class_name => 'User', :foreign_key => 'friend_id'
    end

    of course, it is always nice to have some sort of validation and/or before_create/before_validation that makes sure if you are someone’s friend that they are also your friend. Or maybe you could walk a fine semantic line by pretending that one-way friendships are indications of an outstanding friend request (then delete the one-way upon rejecting, allowing the requester to later reiterate his request).

  3. dnite said

    ya, sorry. I missed that one. I’ll fix it. WordPress doesn’t like to be nice to me in code and blockquote’s .. so something went wrong when I pasted that. I shall fix it now, thanks for the heads up.

  4. dnite said

    Much better. Very sorry about that for anyone else who was confused as well. I completely forgot about the < in the class definition and it killed things. Looks better now. Thanks for letting me know.

  5. zeno said

    when you say:
    “But if I try and do something like… User.find(1).friends”
    you mean…
    “But if I try and do something like… User.find(1).friends

  6. zeno said

    ‘<’

  7. zeno said

    ok, I got it , by posting the previous comment I understood the problem,
    it’s a wordpress problem, not a RoR one ;-) (needed to use “& l t ;” to write “<”)

    so , you should also update the line:

    User.find(1).friends

    with:

    User.find(1).friends << another_user

    (sorry for the “spam”)

  8. dnite said

    ya, everyone’s just catching all my mistakes here. indeed, i meant User.find(1).friends << another_user

  9. Eamon Ford said

    I eagerly await the plugin! :)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
Follow

Get every new post delivered to your Inbox.

%d bloggers like this: