Archive for July 30th, 2007

July 30th, 2007

Be Careful of those Plurals, and other Rails Relationship Gotchas

by Tim Cull

Ok, I’ve been banging my head against this one for a little while, so I want to make sure to share the solution.

In my Ruby on Rails project, I have a RegisteredUser class and a Person class. They have a one-to-one relationship with each other that I was trying to represent like so:

class Person < ActiveRecord::Base
  has_one :registered_users
end
class RegisteredUser < ActiveRecord::Base
  belongs_to :person
end

see the problem? I didn’t either for a while, but maybe the title of this post will give you a clue. The problem is with the pleurality of my has_one. It should be registered_user instead of registered_users. You really have to watch the plurality of relationships in Rails. If it’s one-to-one, then both sides better be singular. If it’s one-to-many, then the many side better be plural and the one side better be singular. It all makes a difference.

The error message I was getting was simply the not-very-helpful:

test_add_reporter_by_name(PersonControllerTest):
NoMethodError: undefined method `registered_user' for #<person :0x48ff2c4>
    d:/ruby/lib/ruby/gems/1.8/gems/activerecord-1.15.1/lib/active_record/base.rb:1848:in `method_missing'
    D:/dev/rails/mystats/config/../app/controllers/person_controller.rb:54:in `add_reporter_by_name'
    d:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.1/lib/action_controller/base.rb:1095:in `send'
    d:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.1/lib/action_controller/base.rb:1095:in `perform_action_without_filters'
...snip...
    d:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.1/lib/action_controller/test_process.rb:353:in `post'
    test/functional/person_controller_test.rb:79:in `test_add_reporter_by_name'

One other gotcha I ran into…if you have a has_and_belongs_to_many relationship (ie. a many-to-many) then you need to make sure your join table in between does not have an identity column. So, in my case I have another many-to-many relationship between RegisteredUser and Person that I call “authorized reporters”. This relationship says which RegisteredUsers are allowed to report a status for which People:

  has_and_belongs_to_many :authorized_reporters, :class_name =>'RegisteredUser', :join_table => 'authorized_reporters'

In between I have a join table called authorized_reporters:

class CreateAuthorizedReporters < ActiveRecord::Migration
  def self.up
    create_table :authorized_reporters, :id => false do |t|
      t.column :registered_user_id, :integer, :null => false
      t.column :person_id, :integer, :null => false
    end
  end
  def self.down
    drop_table :authorized_reporters
  end
end

There is one very important part of that migration: “, :id => false”. Without it, the many-to-many works in one direction, but not in the other (I don’t remember which direction works).