# i'm working on a data model that requires many, many join tables. this is a # pain because of all migrations i need to run and the thinking/re-thinking # that gets done along the way - not to mention setting up all the assocations # in AR by hand for each relationship. i looked into using something like # has_many_polymorphs(http://github.com/fauna/has_many_polymorphs/tree/master) # which is a fantastic plugin i've used before with good success. however # it's a bit too heavy-weight for my use this time - besides all i really want # to do is avoid having a massive proliferation of join tables and crazy-ass # polymorphic fields that make my sql life hell. following is an *extremely* # simple solution i came up with to solve the 'having many "has_many" # relationships and way too many join tables issue. # # the basic concept is to have one mother of a join table through which to # join all related models. the key here, and this is differnt than HMP, is # that our *join* model will use sti, not polymorphic associations and, imho, # this is vastly simpler and more flexible. first, we setup our join table and # it's migration: # class Join < ActiveRecord::Base # here's an example migration for the mother join table # # cfp:~> cat db/migrate/20081024060458_create_joins.rb # CreateJoins = Join::Migration # # or just run # # ./script/runner ' Join::Migration.up ' # class Migration < ActiveRecord::Migration def self.up create_table :joins do |t| t.string :type # sti!!!!!!!!!!!!!!!!!!!!!!!!!!!! t.integer :src t.integer :dst t.timestamps end end def self.down drop_table :joins end end end # now assume we want to associate a Person with multiple parents and multiple # children - normally this would require two join models, but we'll instead # use two STI based join models which can be created ad-hoc, without needing # additional tables or even additional keys on the source models # class Join # maps a person to their children # class PersonChild < Join belongs_to :person, :foreign_key => :src belongs_to :child, :foreign_key => :dst end # maps a person to their parents # class PersonParent < Join belongs_to :person, :foreign_key => :src belongs_to :parent, :foreign_key => :dst end end # now all we have to do is setup the 'normal' has_many through associations, # has_and_belongs_to_many, whatever... # class Person < ActiveRecord::Base has_many :child_joins, :foreign_key => :src, :class_name => 'Join::PersonChild' has_many :children, :through => :child_joins has_many :parent_joins, :foreign_key => :src, :class_name => 'Join::PersonParent' has_many :parents, :through => :parent_joins end class Child; end class Parent; end # vie-oh-la! we can do stuff like this: # # cfp:~/rails_root > ./script/console # # Loading development environment (Rails 2.1.1) # # >> p=Person.create # => #<Person id: 6> # # >> p.parents # => [] # # >> p.parents << Parent.create # => [#<Parent id: 42>] # # >> p.parent_joins # => [#<Join::PersonParent id: 2, type: "Join::PersonParent", src: 6, dst: 42>] # # now, adding more relationships is as simple as # class Person < ActiveRecord::Base # define the join - note that we can do this from anywhere really although # i'd probably keep them all defined in the join model... # class Join::PersonDog < ::Join belongs_to :person, :foreign_key => :src belongs_to :dog, :foreign_key => :dst end # and setup relationships through the sti join model # has_many :dog_joins, :foreign_key => :src, :class_name => 'Join::PersonDog' has_many :dogs, :through => :dog_joins end # UPDATE: i've added a simple class generator so you don't have to even think # about building the join models, it looks like this # class Join def Join.for *args desc = args.shift src, dst, *ignored = desc.to_a.flatten src_options = (args.shift || {}).to_options dst_options = (args.shift || {}).to_options const = "#{ src }_#{ dst }".camelize unless const_defined?(const) join_model = Class.new(Join) const_set const, join_model join_model.module_eval do belongs_to src.to_s.to_sym, src_options.reverse_merge(:foreign_key => :src) belongs_to dst.to_s.to_sym, dst_options.reverse_merge(:foreign_key => :dst) end end "::Join::#{ const }" end end # now you can get the name for a join class and create it all in *one step*, # like so. of course the manual way is still supported. # class Person < ActiveRecord::Base has_many :parent_joins, :foreign_key => :src, :class_name => Join.for(:person => :parent) has_many :parents, :through => :parent_joins end # you can grab the code here - just put it in ./app/models/ # # http://s3.amazonaws.com/drawohara.com.ruby/join.rb #