has_many :through with has_many_polymorphs

20 Dec 2007
by dylan

So, I have been using Evan Weaver’s awesome has_many_polymorphs rails plugin for a while now. It has saved me a lot of frustration, for sure. Evan is also working up a tagging generator for has_many_polymorphs which should help solidify has_many_polymorphs position as the replacement for acts_as_taggable (which is basically deprecated at this point, I think). The plugin is actually much more than just a tagging engine tho. You can have a look at the plugin’s own page for a full (and current) run down of what has_many_polymorphs can do.

For a couple recent projects, I have used has_many_polymorphs to set up has_many :through relationships and it’s actually been such a boon that I reckon it may benefit other folks for me to commit the process to writing.

I am assuming that you have already installed the plugin in your rails app as per Evan’s instructions—it should be just

script/plugin install svn://rubyforge.org/var/svn/polymorphs/has_many_polymorphs

but I am not responsible if the the svn location has changed.

Anyhow, for the purposes of the example, we are setting up an app with a Gallery and Page models that may each have many Photos through an Attachment model. We are also going to set it up so that you can add the joins in the CRUD views for each of the models themselves (in the same params with the rest other model data). Additionally, the joins themselves will be set up for acts_as_list so that if you are creating a gallery, for example, with many photos, you can sort the photos in the gallery (via the join)—I think that’s clear, but it may not be. [Evan actually helped clean up the abstract_id_assignment method below so additional props for that.]

First, the code:

class Photo < ActiveRecord::Base
  has_many_polymorphs :attachables, 
    :from => [:galleries, :pages], 
    :through => :attachments,
    :parent_order => 'attachments.position', 
    :dependent => :destroy

  def abstract_id_assignment(klass_name, ids)
    self.save! 
    ids.map!(&:to_i)
    objs = self.send(klass_name.pluralize)
    objs.delete objs.reject{|obj| ids.include? obj.id}
    objs << klass_name.classify.constantize.find(ids - objs.map(&:id))
  end

  # let's curry some methods
  def gallery_ids= ids
    abstract_id_assignment("gallery", ids)
  end
  def page_ids= ids
    abstract_id_assignment("page", ids)
  end
end
class Attachment < ActiveRecord::Base
  belongs_to :photo
  belongs_to :attachable, 
    :polymorphic => true

  # http://lists.rubyonrails.org/pipermail/rails/2006-May/042362.html
  acts_as_list :scope => 'attachable_id=#{attachable_id} and attachable_type=#{quote_value attachable_type}'
end

The attachment migration would look something like this.

class CreateAttachments < ActiveRecord::Migration
  def self.up
    create_table :attachments do |t|
      t.column :photo_id, :integer, :null => false
      t.column :attachable_id, :integer, :null => false
      t.column :attachable_type, :string, :null => false
      t.column :position, :integer
    end
    add_index :attachments, [:photo_id, :attachable_id, :attachable_type], :unique => true, :name => 'index_attachment_polymorphs' 
  end

  def self.down
    drop_table :attachments
  end
end
class Page < ActiveRecord::Base 
end
class Gallery < ActiveRecord::Base 
end

Okay so now you will probably need to add something like the following to the photo controller.

  def edit
    @photo = Photo.find(params[:id])
    @galleries = Gallery.find(:all)
    @pages = Page.find(:all)
  end

And for the photo/edit action form you can now do something like this.


<ul>Galleries 
    <% @galleries.each do |gallery| %>
    <li><%= check_box_tag "photo[gallery_ids][]", gallery.id, @photo.galleries.include?(gallery) %> <%= gallery.name %></li>
    <% end %>
</ul>
<ul>Pages 
    <% @pages.each do |page| %>
    <li><%= check_box_tag "photo[page_ids][]", page.id, @photo.pages.include?(page) %> <%= page.name %></li>
    <% end %>
</ul>

Oh and if you want the ability to create the joins on views for the page and gallery models, you can do something like the following.

# create a file called lib/activerecord_base_photo_extensions.rb
# this gets required in environment.rb
class ActiveRecord::Base  
  def photo_ids= photo_ids
    photo_ids.map!(&:to_i)
    photos.delete photos.reject{|photo| photo_ids.include? photo.id}
    photos << Photo.find(photo_ids - photos.map(&:id))
  end  
end
# in environment.rb
require 'activerecord_base_photo_extensions'

And I reckon you can now include something like this on the page/edit and gallery/edit forms (though this would probably end up being kind of a ui disaster).

<ul>Photos 
    <% @photos.each do |photo| %>
    <li><%= check_box_tag "page[photo_ids][]", photo.id, @page.photos.include?(photo) %> <%= photo.name %></li>
    <% end %>
</ul>

This is just a very simple example of what is possible with has_many_polymorphs, but the plugin has really saved me a lot of time and headache, so I hope this ends up helping somebody out there.

Top / © 2007-2008 I Am Still Alive. All rights reserved. Information

/ Login