Has_Many :Through - Setting a type on the join table
fancy dancy many-to-many model associations!
I love Ruby! I've always loved Ruby, but today's discovery just reaffirms the validity of my devotion.
I may need to watch my words in the future. My girlfriend (like all girlfriends) doesn't like competition, and I wouldn't want any harm to come to my beloved Ruby.
This handy little trick was picked up from Josh Susser over at has_many :through. His post Magic join model creation goes over a lot of the details, but needed a little updating for Ruby 1.9.1 and Rails 2.0.
We've all run across the situation: two tables, many to many association, but one model can have many 'types' of another.
To better explain what I'm getting at take this example:
People <-n --- n-> Book
A person has many books, a book has many people. Simple enough, right? Well, sort of. I'm sure those people associated with a book can have any number of affiliations that we may want to capture in our data model.
Author <-n --- n-> Book
Editor <-n --- n-> Book
Reviewer <-n --- n-> Book
All of these associations (author, editor, reviewer) are people, and all are associated with our book in some way or another, but creating the individual tables (book_authors, book_editors, book_reviewers) isn't my ideal solution to this problem, and there's a better way.
Instead of creating all of those join tables, lets just make one: "book_people".
create_table :book_people do |t|
t.column :person_id, :integer
t.column :book_id, :integer
t.column :person_type, :string
t.timestamps
end
In model "People".
class Person < ActiveRecord::Base
has_many :book_people, :dependent => :destroy
has_many :books, :through => :book_people
end
In model "Book"
class Book < ActiveRecord::Base
has_many :book_people, :dependent => :destroy
has_many :people, :through => :book_people, :uniq => true
has_many :authors, :through => :book_people, :source => :author, :conditions => ["`book_people`.contact_type = ?","Author"] do
def <<(author)
BookPeople.send(:with_scope, :create => { :contact_type => "Author" } ) { self.concat author }
end
end
has_many :editors, :through => :book_people, :source => :editor, :conditions => ["`book_people`.contact_type = ?","Editor"] do
def <<(editor)
BookPeople.send(:with_scope, :create => { :contact_type => "Editor" } ) { self.concat editor }
end
end
end
And finally, in model "BookPeople"
class BookPeople < ActiveRecord::Base
belongs_to :person
belongs_to :book
belongs_to :author, :class_name => "Person", :foreign_key => :person_id
belongs_to :editor, :class_name => "Person", :foreign_key => :person_id
end
Amazing! Three tables, infinite association types! This model is superior to the many association tables model in a number of ways:
- If you simply want to find out all books a person is associated with, regardless of granularity, person.books works nicely! One table, one query.
- The Person model can be extended. Associations such as person.authored_books, person.edited_books etc. are easy to add.
- Clean database - I always try to get away with as few tables as possible.
Now, I just hope this code works properly.
-Butch
Name:
Website: