Saturday, April 23, 2011

Rails 3: Converting has_and_belongs_to_many to has_many :through


Converting my old Rails 2 app to Rails 3 has been a great education. In this lesson, I had to convert has_and_belongs_to_many to "has_many :through," because as of Rails 3.1, has_and_belongs_to_many with attributes to join the table/model will be deprecated (see comments to this post).So, while the old helper worked fine with Rails 3.0, I decided to be proactive and modernize my app even further.

Once I found the above referenced guide and saw the example about physicians and patients, it seemed straightforward enough. So, why was mine not working? How does one go about debugging it when Rails is throwing exceptions all over the place. And what can possibly go wrong? As I found out, a lot of little things. It's easy to misspell a table reference and never know anything is wrong until the code tries to exercise the association in some way. In one case I found that ":through => 'table_name' " did not work, but ":through => :table_name" did. So, while I got away with using a string in my old code, only a symbol seemed to work in the new code. So, how do I make it easy to debug this stuff?

The first key for me was to use the rails "console," so in my app home directory, I give the command "rails console," and now I have an interactive Ruby session with all my application structures available. In particular I can call upon models to do their thing. I can, for example give the command: User.find(508) to find the user record with id 508. And don't forget about "reload!" which is invaluable for reloading your code in the console session as you're changing it in an editor.

The second key for me was to go through each association one at a time, very methodically, and make sure every necessary component is working. The most fundamental part is the table that provides the two-way association. In my app I have volunteers that attend training sessions. I want to know which training sessions users have attended, and which users have attended which training sessions. So, I have a "training_sessions_users" table. In my schema.rb, I see:

  create_table "training_sessions_users", :id => false, :force => true do |t|
    t.integer  "training_session_id"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.integer  "user_id"

In training_sessions_user.rb then, I need the following two lines of code:
  • belongs_to :user
  • belongs_to :training_session
This creates two simple associations that are easy to test. The following must work, and I made sure I had data to test with:
  • TrainingSessionsUser.first.user
  • TrainingSessionsUser.first.training_session
This is the simplest yet most fundamental part of the whole equation. It's a logical place to start, and I made sure that much worked.

Now for my training sessions. In training_session.rb, I have first and foremost the following:
  • has_many :training_sessions_users, :dependent => :destroy
This means that the following has got to work, and that's the next thing I verified and debugged as necessary:
  • TrainingSession.first.training_sessions_users
Similarly on the user side:
  • has_many :training_sessions_users, :dependent => :destroy
Implies that the following must work, assuming I have a user with id 508:
  • User.find(508).training_sessions_users
So far so good. All of this must be working, but it does not imply that everything I had working using "has_and_belongs_to_many" is still working. So, the next step is to verify that the following found in training_session.rb:
  • has_many :users, :through => :training_sessions_user, :order => 'last_name,first_name,middle_name'
This implies that the following must work:
  • TrainingSession.first.users
Likewise in user.rb:
  • has_many :training_sessions, :through => :training_sessions_users, :order => "date_of_training desc"
Which implies that the following must work:
  • User.find(508).training_sessions
Once I had this verification pattern laid out after my struggles with the first conversion of has_and_belongs_to_many, the next one went relatively smoothly. There were still things to debug. It's so easy to misspell things ever so slightly -- to leave something singular when it needs to be plural, for instance. Or to spell "has_many" as "has_manu," and since you don't see that when you first fire up your application, since it only pops up at runtime, it is imperative to have some methodology to work this out ahead of time. And of course, when you have a patten like the one above, it's easy to encapsulate this in test procedures.