fallenrogue.com

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...

It was written by about 1 year ago.
It has 6932 letters in it.
It has 1011 words in it.
It has a total of 15 comments in all.
So far Leon has the last word!

article Links

These are the links that appear in this article. They probably don't make sense out of context... but just in case. :)

CulturePopp
migrations article
modifying that project
Download the changed files to the miapp project here
Validate that Has_And_Belongs_to_Many relationship in Rails.
 

The other stuff...

What the kids are saying...

about 1 year ago Mike Stephen said...

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!

about 1 year ago av said...

How would you go about persisting the "friend" relationship?

about 1 year ago Jay Phillips said...

Wow, I was just trying to implement friend relationships the other day but didn't have much success with it. It's a miracle how the blogosphere seems to read minds so well and content is created almost as you need it. :)

Keep up the great blogging!

about 1 year ago fallenrogue said...

Thanks guys for your comments! I hope to keep these articles coming and if you've got anything you'd like to see added just send me a note or leave a comment. <br>

@av: I don't know that I understand your question. This technique will persist the data in the database so that the next time you call for friends they will be there. If you'd like to see it in action, just open the console and do this in the development environment.<br>

fred=User.create(:name=>"fred")<br>
sally=User.create(:name=>"sally")<br>
fred.friends << sally<br>

Then exit the console. then go back into the console and run the following<br>

fred=User.find_by_name("fred")<br>
fred.friends<br>

you should see the object for Sally. See? The data has been persisted between calls and saved permanently in the database. Hope that helps! <br>

@Jay - Awesome design on your blog!

about 1 year ago av said...

Where in the database is it stored? The table miapp_test.friends_users is empty.

about 1 year ago fallenrogue said...

Ah, ok! Now I understand the problem. This is what I get for not taking the time to write a better test! :) The only data that is stored in tests is what it loaded from fixtures. So, if you wanted to test this relationship the best way would be to create another fixture called frineds_users.yml and place something like this in it<br>

bob_love_nancy:
user_id: 1
friend_id: 2
nancy_loves_bob:
user_id: 2
friend_id: 1
<br>
and rewrite the test to look like this...<br>
<pre>
require File.dirname(__FILE__) + '/../test_helper'

class UserTest < Test::Unit::TestCase
fixtures :users, :friends_users

def setup
@bob = User.find(1)
@nancy = User.find(2)
end

def test_nancy_likes_bob
assert @nancy.friends.count > 0, "Nancy has no friends"
end

def test_bob_likes_nancy
assert @bob.friends.count > 0, "Nancy has no friends"
end
end
</pre>

Then both fixtures will be loaded into your DB. You'll be able to see the data and run the tests from them. I should have done this from the start and will update the article to reflect this when I get a chance. :) Does that answer your question?

about 1 year ago fallenrogue said...

PS- be careful of cut and paste, some characters were transformed because of how I sanitize comments on the blog. :)

about 1 year ago av said...

Ah, OK, thanks for clarifying. Keep up the great work. :)

about 1 year ago Andre in LA said...

Now onto the real world... How do I add a check for adding a friend twice and a test for it?

Thank you!

about 1 year ago Andre in LA said...

validates_uniqueness_of :friend_id, :scope => :user_id

does not seem to produce an error when adding the same friend twice...

and also for a user adding himself as a friend?

about 1 year ago fallenrogue said...

Alright Andre!! Bringin' the tough questions! ok, the reason that validates_u doesn't work is because the habtm doesn't fire the validators because technically you're not modifying the user model's data... so they never are invoked. Seems like a pretty big exclusion but what are you gonna do? I'll tell you want you're gonna do... you're going to write your own! This may get a little big for the comments, so I'll update the article with a test and full code update tomorrow when I have time but for now just know that this is what we're going for...
<br>
<pre>
has_and_belongs_to_many :friends,
:class_name => "User",
:join_table => "friends_users",
:association_foreign_key => "friend_id",
:foreign_key => "user_id",
:before_add => "make_sure_not_friend"
</pre>
The key is that before_add property, there are 4 of them and they make checking on the HABTM associations a snap... or a hassle, depending how you feel about it! :) Sorry I can't post the full test and code sample just yet, but as soon as I have a second, I'll write and post them as an update. Thanks, Andre for the great question.

about 1 year ago fallenrogue said...

PS- before I write the update, just to get the adventurous of heart get a head-start the four possible callbacks are before_add, after_add, before_remove and after_remove

about 1 year ago fallenrogue said...

I've created a new "part2", if you will, to this article that solves this exact problem, Andre in LA. You can check it out "here":http://www.fallenrogue.com/article/view/144-Validate-that-Has_And_Belongs_to_Many-relationship-in-Rails

10 months ago nyofaze said...

Any advice on how to automate the process of running all the unit tests against the same data? (I want to make sure my changes to my application doesn't break any other parts of the application)

thanks in advance =D

9 months ago Leon said...

Hi nyofaze! I'm not sure exactly what you mean? could you be a little more specific about what you're trying to do? Fixtures should make the data unique and specific for each test as the data is loaded and dumped when they are run.

Are you wanting to "keep" your data? In that case, I actually don't know. I've never tried to persist my test data between testing sessions and don't see why I would. But maybe that's just how I write tests. Anyway, give me some more details and I'll see if I can't be better help. :)

Leave a comment
*name:
*email: (never sold or published.)
url :

©2000-2008 fallenrogue.com | Some Rights reserved.