Ruby on Rails
ManytoManyPolymorphicAssociations (Version #5)

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

has_and_belongs_to_many :products, :join_table => :links, :foreign_key => :linkfrom_id, :association_foreign_key => :linkable_id, :finder_sql => ’SELECT products.* ’ + ’FROM products INNER JOIN links on products.id = links.linkable_id ’ + ‘WHERE links.linkfrom_id = #{id} AND links.linkfrom_type = “Tab” and links.linkable_type = “Product”’, :insert_sql => ’INSERT INTO links (`linkfrom_id`, `linkfrom_type`, `linkable_id`, `linkable_type`) ’ + ‘VALUES (#{id}, “Tab”, #{record.id}, "Product")’, :delete_sql => ‘DELETE FROM links where linkfrom_id = #{id} and linkfrom_type = “Tab” and linkable_id = #{record.id} and linkable_type = “Product”’

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

has_and_belongs_to_many :products, :join_table => :links, :foreign_key => :linkfrom_id, :association_foreign_key => :linkable_id, :finder_sql => ’SELECT products.* ’ + ’FROM products INNER JOIN links on products.id = links.linkable_id ’ + ‘WHERE links.linkfrom_id = #{id} AND links.linkfrom_type = “Tab” and links.linkable_type = “Product”’, :insert_sql => ’INSERT INTO links (`linkfrom_id`, `linkfrom_type`, `linkable_id`, `linkable_type`) ’ + ‘VALUES (#{id}, “Tab”, #{record.id}, "Product")’, :delete_sql => ‘DELETE FROM links where linkfrom_id = #{id} and linkfrom_type = “Tab” and linkable_id = #{record.id} and linkable_type = “Product”’

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>