The great thing about my worklife as of late is seemingly every week I get to learn something new. I don’t necessarily have to learn something new, but I certainly want to. My base of experience is growing and I’m arriving at a place where I can recognize when the unknown thing will likely be worth learning to create a more elegant solution. Thankfully I work with people who have a similar grasp on reality.

World's Biggest (Polymorphic) Bat
 
Photo by merfam

I’ve run across very positive comments about the has_many_polymorphs plugin in the past. Over the past week I’ve been presented with a problem that floated over the plate and begged for me to whack it with my has_many_polymorphs bat. First I had to make the bat.

What problem are you solving?

Glad you asked. The site in question has a snazzy little JavaScript ticker that displays announcements at an interval. I’m not sure if this is clever or just happenstance, but these announcements are not their own thing. In fact, there is much more information to an announcement than just a date and title. Something that is announceable is likely serving a greater purpose elsewhere in the site. And one announceable thing is not necessarily all that similar to another annouceable thing.

has_many_polymorphs allows you to tie together that connecting information into a single table representing the relationship. In it’s basic form, has_many_polymorphs is a has_many relationship where the “many” can be of multiple different types. In the above example, an announcer (snazzy little JavaScript ticker) can announce several different announceables as represented by multiple unique class-types.

Putting has_many_polymorphs together

In reality, the site currently has two announceables: news and events. We’re working with a few models here. A Ticker can announce things. An Event and NewsArticle are announceable. And the Announcement ties things together. Building this should be rote. (Note: we’re not on Edge with this project)

Migration:

class CreateAnnouncements < ActiveRecord::Migration
  def self.up
    create_table :announcements do |t|
      t.column :ticker_id,         :integer, :null => false
      t.column :announceable_id,   :integer, :null => false
      t.column :announceable_type, :string,  :null => false
    end
    add_index :announcements, 
              [:ticker_id, :announceable_id, :announceable_type], 
              :unique => true, 
              :name => 'index_announcement_polymorphs'
  end

  def self.down
    drop_table :announcements
  end
end

Ticker.rb:

class Ticker < ActiveRecord::Base
  has_many_polymorphs :announceables,  :from    => [:events, :news_articles], 
                                       :through => :announcements
                                       
. . .

Announcement.rb:

class Announcement < ActiveRecord::Base
  belongs_to :ticker
  belongs_to :announceable, :polymorphic => true
end

Polymorphic parents and has_many_polymorphs

Of course, it’s never that easy, is it? The ticker isn’t the only announcer in the app. The homepage is also effective at announcing things, thank you very much. So what we really want is a has_many_polymorphs relationship that allows numerous different announcers to announce numerous different announceables. Once I figured this out, it was pretty straightforward.

Migration:

class CreateAnnouncements < ActiveRecord::Migration
  def self.up
    create_table :announcements do |t|
      t.column :announcer_id,      :integer, :null => false
      t.column :announcer_type,    :string,  :null => false
      t.column :announceable_id,   :integer, :null => false
      t.column :announceable_type, :string,  :null => false
    end
    add_index :announcements, 
              [:announcer_id, :announcer_type, :announceable_id, :announceable_type], 
              :unique => true, 
              :name => 'index_announcement_polymorphs'
  end

  def self.down
    drop_table :announcements
  end
end

Ticker.rb (and other announcers):

class Ticker < ActiveRecord::Base
  has_many_polymorphs :announceables,  :from             => [:events, :news_articles], 
                                       :through          => :announcements,
                                       :as               => :announcer,
                                       :foreign_key      => :announcer_id,
                                       :foreign_type_key => :announcer_type

 . . .

Note: Unlike other areas of Rails, you must specify :as, :foreign_key and :foreign_type_key to get the parent polymorphism working.

Announcement.rb:

class Announcement < ActiveRecord::Base
  belongs_to :homepage
  belongs_to :ticker
  belongs_to :announceable, :polymorphic => true
end

Naturally it would be nice to DRY up that has_many_polymorphs declaration since it will be identical for all parents in this polymorphic-polymorphic relationship. But there are other things to DRY up as well, and I’ll do my best in Part II of this series: has_many_polymorphs and checkboxes.

Update

Based on comments, I have DRY’d up the has_many_polymorphs declaration… by removing it entirely. Ticker.rb (and other announcers) no longer needs to do anything to get polymorphic. Announcement.rb has all the code.

class Announcement < ActiveRecord::Base
  belongs_to :announcer,    :polymorphic => true
  belongs_to :announceable, :polymorphic => true
  
  acts_as_double_polymorphic_join :announcers    => [:homepages, :tickers], 
                                  :announceables => [:events, :news_articles]
end

Essentially, “announcers” are parents and “announceables” are children in this two-way polymorphic relationship. In thinking of “announcers” as parents, I also got caught singularizing the symbols in that array (e.g. [:homepage, :ticker]). Dontdothatitwontwork. Pluralize all receivers of the polymorph.

Sep 19, 2007 · Subscribe · Archive · Projects · Twitter · GitHub · Flickr