fallenrogue.com

Validate that Has_And_Belongs_to_Many relationship in Rails

OK, big props to “Andre in LA” who in my last article about self-referential joins asked a great question in two parts with the gist being

“How do I add a check for adding a friend twice and a test for it?”
he then mentioned that
“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?”

Damn fine investigative work, Andre in LA, and one of the biggest points of frustration for me when building CulturePopp. In fact, in CulturePopp, because I wanted to build that thing in an afternoon I didn’t take the time to actually place this check where it belongs, in the model, I instead added the logic for managing this in my views. I’m still ashamed. :) The reason being, that kind of logic belongs with the model, after all, that’s where you validate them on creation and update, right? right. That’s what I thought with HABTM as well, but that’s not exactly correct. The reason that the validator did not catch Andre’s addition is because the validators for models only fire with you’re creating or updating the actual data representation of the model itself. Since the following does not affect the User class directly…
@user.friends << @other_user

No User validators are EVER going to be called! That’s why in Rails 0.13 we got what are known as Association Callbacks. There are four of them and they allow you to execute methods either before or after you remove or add an association. The four available are:
before_add
after_add
before_remove
after_remove

You can think of them as before/after filters for your model associations and they go right on with the association constructor. So, if you’ve got your miapp User model open, modify it to look like this and we’ll discuss it when you’re done…
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",
    :before_add => :adding_relationship

  def adding_relationship(friend)
    if self.friends.include? friend
      raise Exception.new(friend.name+" is already your friend.")
    elsif friend.id == self.id
      raise Exception.new("You cannot befriend yourself.")
    end
  end
end

Ok, so we added in the friends relationship from the last article (don’t worry, I’m uploading an updated project at the end of the article) adding in a “before_add” callback.

There are a couple of very important things to note here. First, I’m using a symbol for the method name in the habtm constructor. If you use a string you will cause an error to be throw, so remember these callbacks expect a symbol on the right side of the arrow. Second, to achieve Andre’s desired affect which was to block this behavior altogether we need to raise a new exception which is the only way to prevent the code from completing its task of adding the relationship in the database. I take that information from the documentation so if I’ve got that wrong, please someone comment and let me know because it makes the test… AWESOME and fun to write!!!!

Now, you’ve got a system that will check before adding a friend to your User model if it is the same user or a friend that you’ve already got. But how do we test if an Exception is thrown? Well, you just catch the Exception and make sure it’s the one you’re expecting! So, let’s write a test! Here is our updated Unit test for the User model…
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" 
    begin
      @nancy.friends << @bob 
    rescue Exception
      assert_equal @bob.name+" is already your friend.", $!.to_s
    end
  end

  def test_bob_likes_nancy
    assert @bob.friends.count > 0, "Bob has no friends" 
    begin
      @bob.friends << @nancy 
    rescue Exception
      assert_equal @nancy.name+" is already your friend.", $!.to_s
    end
  end
end

Pretty cool, right? So, first we check to make sure that each of our users has a friend. Then we want to break them! We could easily replace the begin/rescue block with a throw/catch block but I didn’t… so go ahead if you want to! In this example I purposely create an error then I rescue the error so that my test doesn’t bomb out. Rather than just sitting back and being content that an error has been thrown, we do something far more useful, we actually look for the exact error message that we threw. That’s pretty awesome in my book.

So, there you have it. I wouldn’t say it’s the cleanest or easiest thing to implement but pretty cool when you think about it and know what to expect. Please feel free to share your tests and tips, leave questions or thoughts in the comments and click here to download the updated miapp project.

UPDATE

I've just had a great email, the ones that I actually enjoy reading, from a guy named Duncan who can be found here: whomwah.com who chimes in with a nice alternative to raising random exceptions. The bonus is that by raising the Rails specific exceptions you can catch them the way that you're already doing with your models! It's a great tip and I thought I'd share it with you... This comes from Duncan and I've not run it so use at your own peril! :)

def adding_relationship(friend)
    if self.friends.include? friend
      errors.add_to_base(friend.name+" is already your friend.")
      raise ActiveRecord::RecordInvalid, self
    elsif friend.id == self.id
      errors.add_to_base("You cannot befriend yourself.")
      raise ActiveRecord::RecordInvalid, self
    end
  end
Duncan then implements like so...
 obj.friends << friend
   obj.save!
rescue ActiveRecord::RecordInvalid => e
   render :action => :new
end
Thanks to Duncan for sharing!

articleStats

Here are some silly little facts about this Validate that Has_And_Belongs_to_Many relationship in Rails...

It was written by about 1 year ago.
It has 6959 letters in it.
It has 975 words in it.
It has a total of 8 comments in all.
So far Ida 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. :)

about self-referential joins
CulturePopp
miapp
click here to download the updated miapp project.
whomwah.com
 

The other stuff...

What the kids are saying...

about 1 year ago Andre in LA said...

Thank you for demonstrating fixtures, tests, and "hacking" the ActiveRecord in a simple enough way for my overloaded brain to understand.

Now my next question: what was the flow of your learning ruby+rails? How did you first discover/know about the callbacks for the habtm method? How did you learn about the ".include?" method?

The reason I am asking is this: I have an understanding of how the Rails framework works, of simple Ruby scripting, but every time I face a real-world application, I end up looking for/needing things which I am not sure exist and often unclear of what they would be called and stop before I can jump to the next level of understanding/complexity/fluency in Ruby.

Thanks again,
-- Andre in LA

about 1 year ago fallenrogue said...

Andre, let me answer in reverse. I completely understand your pain and that's what I'm trying to do with these articles, take day-to-day coding problems and share how I tackled them. <br>

As for my learning process I have found that the "Programming Ruby":http://www.rubycentral.com/book/lib_standard.html book has been my best friend whenever I have a ruby syntax question. The real "pain point" for me has been knowing what's available in Rails when trying to solve a problem. Let me give you an example. To say that the documentation is lacking is true and the best manual available is the book "Agile Web Development with Rails":http://www.pragmaticprogrammer.com/titles/rails2/index.html but unlike the framework you have to pay for it. That said, it's the best 20 dollars I ever spent when learning Rails. <br>

Finally, I know it's tedious but this is how I tackle a problem in Rails: First, I check the api (api.rubyonrails.org) and if that fails me: Second, I open up Rails itself and look through the source code to find how they created something in Rails. If that fails I create my own modules or write my own code the solve the problem... which usually leads to me finding someone else who has fixed it! :) <br>

I found .include? in the (out-of-date) documentation for found for has_and_belongs_to_many. All I can say is keep trying and your Ruby-Fu will become strong! The community for rails is small but growing every day. If you get stuck there are many guys out there trying to help, myself included. (ps- you can send email through the contact link at the top of this site.) <br>

I hope that answered your question. All I can say is that I love Ruby and I love Rails sometimes that means that I have to look high and low for an answer or hack my own solution together but it's worth it for all of the time I save and joy I feel when using it.

about 1 year ago Andre in LA said...

Thank you, I've been taking the "api- then ruby-lang then the online programming ruby book" approach but it is frustrating to me to have to look in different places. I also started using the ruby+rails Google coop at http://www.rubyinside.com/search/

Interestingly, the lack of unified, up-to-date documentation, especially for the framework, sends me on a treasure hunt and I find unrelated gems (like this blog).

about 1 year ago fallenrogue said...

It's certainly frustrating but with time and practice comes mastery and hell, if you ever have a question I'm more than happy to give you my two cents just send me an email. The Rails community is small but growing quickly if I can continue to be a "gem" with these articles I'm more than happy to keep writing them. I love Rails and I hope these articles reflect that. Thanks for your comments Andre, they have sparked some great articles and tips for the users out there!

10 months ago Nicole said...

Well, I cant agree more.

8 months ago Justin said...

So, now I wonder. Would you continue to use a habtm relationship or would you use has_many :through now?

8 months ago Leon said...

Hi Justin! First of all, you've got to know that this article was written a long time ago and therefore may have elements that have fallen out of best practice. The technique is still valid but the best practice will continue to evolve.

That said, I still find habtm very useful in specific situations where you don't track the relationship as an entity or your needs are not polymorphic in nature.

If you're thinking you might want to go that way OR you like the h_m :t syntax I don't think it's a bad way to go for every possible relationship. AND, in the scenario above, I've since written applications using a similar model association as h_m :t. So, just like any choice in development it's the needs of the app should drive the choice made.

So, I don't know if that answers your question but it certainly does inspire me to rethink some of the popular "archive" articles on this site and possibly doing a series of new versions for Rails 2.*. How about that? Since I'm doing several re-writes for 2.0 now would certainly be a good time for it.

thanks for the comment, Justin, and please let me know how you feel on the topic!

4 months ago Ida said...

He who pays the piper calls the tune,

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

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