I am what I am or self-referential joins in Rails
Ok, this is a little more advanced than my last two articles but if you’ve been following along with those then you’re more than ready to take it up a notch to something a little more difficult. Now, this is Rails so it’s really not that difficult at all. It does assume some familiarity with has_and_belongs_to_many models.
Recently, I was asked how I do the “friends” features in CulturePopp which is a digg clone for celebrity news that I put together on a bet. (the bet was that digg could essentially be created in an afternoon. I won.) It’s a good question. You’ve got two “objects” in the traditional sense, users and their friends, both of which are actually user objects. If you were doing this in a traditional app, you’d have one table for the users and another that that tracked the friends of that user. In Rails this is exactly the same, it’s just we need to know how to do it the Rails way. We’re going to use the “miapp” project from the migrations article so you can simple cut and paste all of the code here or type along while modifying that project ok, let’s begin!
We already have users now we just want to be able to know who their friends are by calling out User.friends which will return a set of User objects. First we need a way to track the users. To do this… we use a migration! To the console!
ruby script/generate migration create_friends_users
Open the new migration file. What we’re going to do is create a “has_and_belongs_to_many” type relationship but instead of linking 2 models, we’re going to use one… but give it an alias. So, let’s create the table in habtm style
class CreateFriendsUsers < ActiveRecord::Migration
def self.up
create_table :friends_users, :id=>false do |t|
t.column :user_id, :integer
t.column :friend_id, :integer
end
end
def self.down
drop_table :friends_users
end
end
If you’ve done habtm relationships before, then this code is not going to surprise you. The only surprise should be “friend_id”... because… we don’t have a Friend model with a corresponding friend_id!!! Well, remember, our friends are just users anyway… so let’s tell our model how to build a friend. I’m going to paste in the code for the entire user model we’ve created thus far and explain after…
class User < ActiveRecord::Base
has_and_belongs_to_many :roles
has_and_belongs_to_many :friends,
:class_name => "User",
:join_table => "friends_users",
:association_foreign_key => "friend_id",
:foreign_key => "user_id"
end
See. It’s not that bad at all, right? I don’t even need to explain it. You’re all just freaking out that I even decided to write this down, right? :) Well, if not let me go into detail. We’ve create a habtm relationship with “friends”, we then specify the class_name that the results will “render as” (they are friends but under the hood they are User objects). This is similar to inheritance and you could argue actually is… but that’s a debate for a different day. Next because the join is not happening on two models we have to tell it what the table is to join Users to other Users but as friends. Then we tell rails which is the main foreign key and the association foreign key.
It’s literally as simple as that. Now… do you want to know if it’s working? :) You could fire up the Console and create 2 users and make them friends but we want to do it Rails style, right? So, let’s create a test. Wait! Where are you going? It’s going to be easy and you’ll love it! It’s going to save you time and make life easy, I promise! well, at the very least it will make it really easy to test if you get fancy and make changes. OK? Here we go!
Open test/fixtures/users.yml and following the formatting to make 2 new users for your test database. (Ps- if you haven’t created a test database yet, go ahead and do that. The development one won’t work for this.) Here’s my YAML file for users:
bob: id: 1 name: 'bob' password: 'pass' email: 'dummy@fake.com' nancy: id: 2 name: 'nancy' password: 'pass' email: 'fake@dummy.com'
Nothing fancy just 2 users. Now let’s write a test. Since we’re testing our model it’s a unit test. So open file test/unit/user_test.rb add create a test. Granted, I’m just throwing this together and the test could (should) be written with more useful results. But it’s late, I’m tired from playing Gears of War all night and we just want to make sure that we can add friends so here we go.
require File.dirname(__FILE__) + '/../test_helper'
class UserTest < Test::Unit::TestCase
fixtures :users
def setup
@bob = User.find(1)
@nancy = User.find(2)
end
# Replace this with your real tests.
def test_nancy_likes_bob
@nancy.friends << @bob
assert @nancy.friends.count > 0, "Nancy has no friends"
end
def test_bob_likes_nancy
@bob.friends << @nancy
assert @bob.friends.count > 0, "Nancy has no friends"
end
end
Please note that I added a method “setup” test files recognize this method at runtime and will execute it first if it exists. You can load any variables that your test may run here. Now just prepare the test database and run the test.
rake db:test:prepare ruby test/unit/user_test.rb
You should get a similar message to…
2 tests, 2 assertions, 0 failures, 0 errors Loaded suite test/unit/user_test Started .. Finished in 0.477995 seconds.
And that’s it. You’ve just created a self referential join in Rails as well as a test to make sure it works. Next, we better had some hashing to that password field! :) I encourage you to try and come up with some tests for this example and maybe share a few of your favorites in the comments.
Download the changed files to the miapp project here
UPDATE: I’ve continued to explore this subject in a sorta part 2 called Validate that Has_And_Belongs_to_Many relationship in Rails.
articleStats
Here are some silly little facts about this I am what I am or self-referential joins in Rails...
article Links
These are the links that appear in this article. They probably don't make sense out of context... but just in case. :)
CulturePoppmigrations article
modifying that project
Download the changed files to the miapp project here
Validate that Has_And_Belongs_to_Many relationship in Rails.
Love your tutorials, keep up the good work! I love the way that you can use ActiveRecord very simply if you want to do the norm, but it doesn't get ugly when you need to do more!