# # url: http://s3.amazonaws.com/drawohara.com.snippets/joins_rb.html # src: http://s3.amazonaws.com/drawohara.com.ruby/joins.rb # # file: RAILS_ROOT/lib/joins.rb # # using joins.rb you can escape join model hell. never again will you need to # generate countless join tables for every has_many or has_one relationship. # joins.rb is both a class generator and association generator in 59 lines of # code that manages all joins via a single sti based db table, allowing you to # develop rapidly while never prohibiting you from moving to dedicated join # models should your table space feel too roomy. usage is as follows # # class Child < ActiveRecord::Base # has_many :parents, :through => join(:child => :parent) # end # # class Parent < ActiveRecord::Base # has_many :children, :through => join(:parent => :child) # end # # notice the direction if the relationship is declared via what's on the # lhs/rhs of the ':key => :val' pair. let's break down what those two calls # do: # # * has_many :parents, :through => join(:child => :parent) # # . creates a class Join::ChildParent. the naming is always determined # via the lexical sort! no pluralization rules are ever applied. # # . creates a has_many association named in the order of arguments, in # this case # # has_many( # :child_parent_joins, # :foreign_key => :src, # :class_name => '::Join::ChildParent' # ) # # * has_many :children, :through => join(:parent => :child) # # . creates a class Join::ChildParent. the naming is always determined # via the lexical sort! no pluralization rules are ever applied. # # . creates a has_many association named in the order of arguments, in # this case # # has_many( # :parent_child_joins, # :foreign_key => :dst, # :class_name => '::Join::ChildParent' # ) # # notice that each through association shares the join model, yet builds a # directional (named like it was specified) has_many join assocications on the # target class. the join model itself is stored in the single join db table # which you can migrate using something like # # ./script/runner ' Join::Migration.up ' # # or with a line like this in a migration # # CreateJoins = Join::Migration # # of course you need to put the file RAILS_ROOT/lib and add # # require 'joins' # # to config/environment.rb or one of your initializers # # that's it. enjoy! # class Join < ActiveRecord::Base module Method def join *args desc = args.shift src, dst, *ignored = desc.to_a.first src_options, dst_options, *ignored = args src = src.to_s.underscore dst = dst.to_s.underscore model = Join.model_for(src, dst, src_options, dst_options) joins = [src, dst, 'joins'].join('_') foreign_key = [src,dst] == [src,dst].sort ? 'src' : 'dst' class_name = model.name has_many( joins, :foreign_key => foreign_key, :class_name => class_name, :uniq => true, :dependent => :destroy ) joins end end ::ActiveRecord::Base.send :extend, Method Models = {} def Join.model_for src, dst, src_options=nil, dst_options=nil src = src.to_s.underscore dst = dst.to_s.underscore src_options = (src_options||{}).to_options dst_options = (dst_options||{}).to_options if [src, dst] != [src, dst].sort src, dst = dst, src src_options, dst_options = dst_options, src_options end model_name = [src, dst].join('_').camelize return Models[model_name] if Models.has_key?(model_name) model = Class.new(Join) const_set model_name, model Models[model_name] = model 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 return Models[model_name] end class Migration < ActiveRecord::Migration def self.up create_table :joins, :force => true do |t| t.string :type # sti t.integer :src t.integer :dst # t.timestamps end add_index :joins, [:type] add_index :joins, [:src] add_index :joins, [:dst] add_index :joins, [:src, :dst], :unique => true end def self.down remove_index :joins, [:src, :dst] remove_index :joins, [:dst] remove_index :joins, [:src] remove_index :joins, [:type] drop_table :joins end end end