Ruby on Rails
ManytoManyPolymorphicAssociations

Most of the examples and usage of the 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:

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`)
)

Here is the Link class, although I realized it’s not really necessary.
class Link < ActiveRecord::Base
  belongs_to :linkable, :polymorphic => true
  belongs_to :linkfrom, :polymorphic => true
end

Having the link class allows us to do something like this:

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:

cat = Category.new
prod = Product.new
cat.products << prod
cat.products
#we get [<prod object>]
cat.products.delete(prod)
#we get []

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:

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:

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

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