My troubles with many-to-many

by Tim Cull

New version of MyStats!. In this version, when you add new people to your group, they actually get an email asking them to sign up. Also, there are many other usability improvements.

So, a long time ago I promised to describe a problem I was having with a many-to-many relationship. You’ll need to strap on your seatbelt, because this one takes some explanation…

I’ve got this many-to-many relationship from a RegisteredUser to a Person that basically says “this registered user is allowed to report the status of these people.” Why do I have different classes for a registered user and a plain, old person? Because I want registered users to be able to keep track of other people who aren’t necessarily users of my application (like, say, my 18 month old daughter). You can think of RegisteredUser as a specialization of Person and, in fact, if I were doing things over again I would have made that literally so (my failure here will become important).

So anyway, I wanted to make this relationship bi-directional, so that for each member of this:

a_registered_user.authorized_reportees

the following would be true:

a_person.authorized_reporters.include?(a_registered_user)

Sounds pretty easy, right? I just had to model it like this:

class RegisteredUser < ActiveRecord::Base
  belongs_to :person
  has_and_belongs_to_many :authorized_reportees, :class_name => 'Person',
        :join_table => 'authorized_reporters'
end

class Person < ActiveRecord::Base
  has_and_belongs_to_many :authorized_reporters, :class_name =>'RegisteredUser',
        :join_table => 'authorized_reporters'
  has_one :registered_user
end

Turns out that didn’t work and it all hinges (I think) on the fact that I’ve got two relationships between RegisteredUser and Person:
1. RegisteredUser.person/Person.registered_user which is where RegisteredUser acts as a specialization of Person, and
2. RegisteredUser.authorized_reportees/Person.authorized_reporters which is the many-to-many.

With this setup, what would you predict the first name in this code is? The first name of “@person” or the first name of “reporter”?:

for reporter in @person.authorized_reporters
	reporter.person.first_name
end

What I meant it to be was the first name of “reporter” but through the magic that is ActiveRecord what it ended up being was the first name of “@person”. I still don’t know why, but I do know that once I changed the code to this, I got what I wanted:

class RegisteredUser < ActiveRecord::Base
  belongs_to :person
  has_and_belongs_to_many :authorized_reportees, :class_name => 'Person',
        :join_table => 'authorized_reporters',
        :foreign_key => 'authorized_registered_user_id',
        :association_foreign_key => 'authorized_person_id'
end

class Person < ActiveRecord::Base
  has_and_belongs_to_many :authorized_reporters, :class_name =>'RegisteredUser',
        :join_table => 'authorized_reporters',
        :foreign_key => 'authorized_person_id',
        :association_foreign_key => 'authorized_registered_user_id'
  has_one :registered_user
end

I had to explicitly specify the foreign keys and change the names of the database columns from person_id to authorized_person_id. Then everything worked fine. Go figure.

Bookmark and Share

Leave a Reply