polymorphic :as interface are only polymorphic on one side of the relationship. I wanted something that was polymorphic on both sides of the relationship: anyobject links to anyobject.
(For an alternate implementation than below see has_many_polymorphs http://blog.evanweaver.com/pages/has_many_polymorphs)
Create a polymorphic many to many join table:
<pre>
CREATE TABLE `links` (
`id` int(11) NOT NULL auto_increment,
`linkfrom_type` varchar(255) default NULL,
`linkfrom_id` int(11) default NULL,
`linkable_type` varchar(255) default NULL,
`linkable_id` int(11) default NULL,
PRIMARY KEY (`id`)
)
</pre>
Here is the Link class, although I realized it’s not really necessary.
<pre>
class Link < ActiveRecord::Base
belongs_to :linkable, :polymorphic => true
belongs_to :linkfrom, :polymorphic => true
end
</pre>
Having the link class allows us to do something like this:
<pre>
my_link_from_object = AnObject.create
my_link_to_object = AnObject.create
new_link = Link.create
new_link.linkfrom << my_link_from_object
new_link.linkable << my_link_to_object
new_link.save
It works, but that is not too intuitive. I don’t want to manage my relationships through the link class, I want to manage it through the object in question, whatever it may be. I.e., I would rather do something like this:
<pre>
cat = Category.new
prod = Product.new
cat.products << prod
cat.products
#we get [<prod object>]
cat.products.delete(prod)
#we get []
</pre>
Well…, try as I might with :through and :as, I couldn’t get the above nicities out of active record, it kept throwing errors. (If you have a better way please let me know, but this is how I got it to work).
What I want is all the cool active record associations that we get with has_and_belongs_to_many, except I want it double polymorphic. Well it turns out has_and_belongs_to_many allows you to rewrite the finder_sql, delete_sql and insert_sql. All the CRUD methods except update, which we don’t need.
So for a class Tab to have many Products we define a has_and_belongs_to_many relationship with products, and specifically tell it how to get the record it needs by passing the polymorphic field names, ids and types that the sql needs to fetch the correct records:
<pre>
class Tab < ActiveRecord::Base
end
And voila, everything seems to work (doesn’t work on edge rails though). No I haven’t written any tests. You’re welcome to if you want.
It’s kinda hellish to have to write all that sql for every class that you want to enable to use the polymorphic join table, so I wrote a plugin that writes the above has_and_belongs_to_many code for me.
I call it: via_polymorphs_has_and_belongs_to_many which is a wrapper for the regular has_and_belongs_to_many. Simply pass it the associated object name, the link table, and the two polymorphic :as interfaces used by the table.
Just add this line to a class to use the join table:
<pre>
class Tab < ActiveRecord::Base
via_polymorphs_has_and_belongs_to_many :products, :polymorphic_join_table => :links, :polymorphic_from => :linkfrom, :polymorphic_to => :linkable
end
Here is the plugin code: Put it in a file in a folder in your plugins directory and don’t forget to add an init.rb
<pre>
module ActiveRecord
module Associations # :nodoc:
module ClassMethods
def via_polymorphs_has_and_belongs_to_many(association_id, options={})
pjoin = options[:polymorphic_join_table]
#from polymorphic column field names
pfrom = {:id_label => (options[:polymorphic_from].to_s + '_id').to_sym,:type_label => (options[:polymorphic_from].to_s + '_type').to_sym, :type => self}
#to polymorphic column field names
pto = {:id_label => (options[:polymorphic_to].to_s + '_id').to_sym,:type_label => (options[:polymorphic_to].to_s + '_type').to_sym,:type => association_id.to_s.classify.to_s, :table => association_id}
class_eval do
has_and_belongs_to_many pto[:table],
{:join_table => pjoin,
:foreign_key => pfrom[:id_label],
:association_foreign_key => pto[:id_label],
:finder_sql =>
"SELECT #{pto[:table]}.* " +
"FROM #{pto[:table]} INNER JOIN #{pjoin} on #{pto[:table]}.id = #{pjoin}.#{pto[:id_label]} " +
"WHERE #{pjoin}.#{pfrom[:id_label]} =" + ' #{id} ' +
" AND #{pjoin}.#{pfrom[:type_label]} = '#{pfrom[:type]}' and #{pjoin}.#{pto[:type_label]} = '#{pto[:type]}'",
:insert_sql =>
"INSERT INTO #{pjoin} (`#{pfrom[:id_label]}`, `#{pfrom[:type_label]}`, `#{pto[:id_label]}`, `#{pto[:type_label]}`) " +
'VALUES (#{id}, ' + "'#{pfrom[:type]}', " + '#{record.id}, ' + "'#{pto[:type]}')",
:delete_sql =>
"DELETE FROM #{pjoin} WHERE #{pfrom[:id_label]} = " +
'#{id}' + " AND #{pfrom[:type_label]} = '#{pfrom[:type]}' AND #{pto[:id_label]} = " +
'#{record.id}' + " AND #{pto[:type_label]} = '#{pto[:type]}'"
}
end #end clas_eval
end #end def
end #end classmethods
end #associatioins
end #end active record
</pre>
polymorphic :as interface are only polymorphic on one side of the relationship. I wanted something that was polymorphic on both sides of the relationship: anyobject links to anyobject.
(For an alternate implementation than below see has_many_polymorphs http://blog.evanweaver.com/pages/has_many_polymorphs)
Create a polymorphic many to many join table:
<pre>
CREATE TABLE `links` (
`id` int(11) NOT NULL auto_increment,
`linkfrom_type` varchar(255) default NULL,
`linkfrom_id` int(11) default NULL,
`linkable_type` varchar(255) default NULL,
`linkable_id` int(11) default NULL,
PRIMARY KEY (`id`)
)
</pre>
Here is the Link class, although I realized it’s not really necessary.
<pre>
class Link < ActiveRecord::Base
belongs_to :linkable, :polymorphic => true
belongs_to :linkfrom, :polymorphic => true
end
</pre>
Having the link class allows us to do something like this:
<pre>
my_link_from_object = AnObject.create
my_link_to_object = AnObject.create
new_link = Link.create
new_link.linkfrom << my_link_from_object
new_link.linkable << my_link_to_object
new_link.save
It works, but that is not too intuitive. I don’t want to manage my relationships through the link class, I want to manage it through the object in question, whatever it may be. I.e., I would rather do something like this:
<pre>
cat = Category.new
prod = Product.new
cat.products << prod
cat.products
#we get [<prod object>]
cat.products.delete(prod)
#we get []
</pre>
Well…, try as I might with :through and :as, I couldn’t get the above nicities out of active record, it kept throwing errors. (If you have a better way please let me know, but this is how I got it to work).
What I want is all the cool active record associations that we get with has_and_belongs_to_many, except I want it double polymorphic. Well it turns out has_and_belongs_to_many allows you to rewrite the finder_sql, delete_sql and insert_sql. All the CRUD methods except update, which we don’t need.
So for a class Tab to have many Products we define a has_and_belongs_to_many relationship with products, and specifically tell it how to get the record it needs by passing the polymorphic field names, ids and types that the sql needs to fetch the correct records:
<pre>
class Tab < ActiveRecord::Base
end
And voila, everything seems to work (doesn’t work on edge rails though). No I haven’t written any tests. You’re welcome to if you want.
It’s kinda hellish to have to write all that sql for every class that you want to enable to use the polymorphic join table, so I wrote a plugin that writes the above has_and_belongs_to_many code for me.
I call it: via_polymorphs_has_and_belongs_to_many which is a wrapper for the regular has_and_belongs_to_many. Simply pass it the associated object name, the link table, and the two polymorphic :as interfaces used by the table.
Just add this line to a class to use the join table:
<pre>
class Tab < ActiveRecord::Base
via_polymorphs_has_and_belongs_to_many :products, :polymorphic_join_table => :links, :polymorphic_from => :linkfrom, :polymorphic_to => :linkable
end
Here is the plugin code: Put it in a file in a folder in your plugins directory and don’t forget to add an init.rb
<pre>
module ActiveRecord
module Associations # :nodoc:
module ClassMethods
def via_polymorphs_has_and_belongs_to_many(association_id, options={})
pjoin = options[:polymorphic_join_table]
#from polymorphic column field names
pfrom = {:id_label => (options[:polymorphic_from].to_s + '_id').to_sym,:type_label => (options[:polymorphic_from].to_s + '_type').to_sym, :type => self}
#to polymorphic column field names
pto = {:id_label => (options[:polymorphic_to].to_s + '_id').to_sym,:type_label => (options[:polymorphic_to].to_s + '_type').to_sym,:type => association_id.to_s.classify.to_s, :table => association_id}
class_eval do
has_and_belongs_to_many pto[:table],
{:join_table => pjoin,
:foreign_key => pfrom[:id_label],
:association_foreign_key => pto[:id_label],
:finder_sql =>
"SELECT #{pto[:table]}.* " +
"FROM #{pto[:table]} INNER JOIN #{pjoin} on #{pto[:table]}.id = #{pjoin}.#{pto[:id_label]} " +
"WHERE #{pjoin}.#{pfrom[:id_label]} =" + ' #{id} ' +
" AND #{pjoin}.#{pfrom[:type_label]} = '#{pfrom[:type]}' and #{pjoin}.#{pto[:type_label]} = '#{pto[:type]}'",
:insert_sql =>
"INSERT INTO #{pjoin} (`#{pfrom[:id_label]}`, `#{pfrom[:type_label]}`, `#{pto[:id_label]}`, `#{pto[:type_label]}`) " +
'VALUES (#{id}, ' + "'#{pfrom[:type]}', " + '#{record.id}, ' + "'#{pto[:type]}')",
:delete_sql =>
"DELETE FROM #{pjoin} WHERE #{pfrom[:id_label]} = " +
'#{id}' + " AND #{pfrom[:type_label]} = '#{pfrom[:type]}' AND #{pto[:id_label]} = " +
'#{record.id}' + " AND #{pto[:type_label]} = '#{pto[:type]}'"
}
end #end clas_eval
end #end def
end #end classmethods
end #associatioins
end #end active record
</pre>