Saturday, April 24, 2010

Using has_many :through for a Triple Join

I'm in the process of coding a Rails program and came across a situation where I need to define relationships between three tables that have one overall join table. I looked for more than an hour to find a good example on the internet and never found one that fully explained what I needed. Finally, I was able to piece together a couple snippets from different places.

Since I had such a hard time finding what I needed, I've decided to write up my problem and my solution in detail in hopes that it will help someone else out there who's looking for the same thing I did. I apologize in advance that I can't remember where I got some of the information, but I'll try to give credit if I can.


The Problem
I want to create the following tables and set up the relationships in Rails:
( ... ~ more data in the table)

people
id
name
...
properties
id
name
...
property_types
id
name
...
people_properties
person_id
property_id
property_type_id


Does Your Data Fit?
Think about the relationships like so:

 A  belongs to 0 or more  B  and  B  has 0 or more  A  with  C .

Therefore, my example looks like:

A property belongs to 0 or more people and a person has 0 or more properties with a property type.

If your data fits the general formula, then this is the solution you're looking for.



Creating the Scaffolds/Models in Rails
Depending on how your program is set up, you'll either generate a scaffold or make it simpler with just generating the model.  For this exercise, I'll build scaffolds for all the tables and a model for the join table.  The '...' indicate that you can add more columns to the tables if you'd like.

ruby script/generate scaffold person name:string ...

ruby script/generate scaffold property name:string ...

ruby script/generate scaffold property_type name:string ...

ruby script/generate model people_property person_id:integer property_id:integer property_type_id:integer

At this point, you can open the migration files for any of the models above and include any additional information such as creating an index for the people_properties join table.  You can probably set :id => false for the join table since there will be no need for rails to look at ids for the join table.


Creating the Relationships

The has_and_belongs_to_many method won't work for this scenario (and most rails programmers seem to dislike the HABTM method), so we're going to use has_many, belongs_to and has_many :through methods to set up our relationships.

Using the generic formula from above, think about setting up your relationships like so:

A has many Joined elements.
A has many B through the Joined elements.

B has many Joined elements.
B has many A through the Joined elements.

C has many Joined elements.

Joined elements belong to A.
Joined elements belong to B.
Joined elements belong to C.

Using my example data of People, Properties, Property Types, and People's Properties (a.k.a Joined elements), each model file should look like:

class Person < ActiveRecord::Base
has_many :people_properties
has_many :properties, through => :people_properties
end

class Property < ActiveRecord::Base
has_many :people_properties
has_many :people, through => :people_properties
end

class PropertyType < ActiveRecord::Base
has_many :people_properties
end

class PeopleProperty < ActiveRecord::Base
belongs_to :person
belongs_to :property
belongs_to :property_type
end


Putting It All Together
Just type rake db:migrate and you should be good to go!


Sources
Triple join in Ruby on Rails [Stack Overflow]
Do I need to manually create a migration for a HABTM join table? [Stack Overflow]
Same model, multiple has_many :through? [Rails Forum]
has_and_belongs_to_many in Rails [Stack Overflow]

Disclaimer
I'm a Rails newbie and might have made some mistakes or provided a solution that might be overkill.  All I know is after searching for awhile, this is what I finally came up with that worked for me.  If you have any suggestions for improvements, please let me know.