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.
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:
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:
has_many_polymorphs :announceables, :from => [:events, :news_articles],
:through => :announcements
. . .
Announcement.rb:
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:
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):
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:
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.
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.
Technorati Tags: Rails, Ruby on Rails, has_many_polymorphs
Related posts:







Comments (6)
You might want to look at acts_as_double_polymorphic_join, which makes this a whole lot easier still!
There’s acts_as_double_polymorphic_join for just this reason, if I understand you correctly.
acts_as_double_polymorphic
:)
(Ed: cleaned up the link a bit)
Thanks, guys. I’ll take a look at at it. I realized it existed, but I didn’t feel it directly related to what I’m doing.
I think my confusion is in the cross-polination of class-types in the example:
class Devouring < ActiveRecord::Base belongs_to :eater, :polymorphic => true belongs_to :eaten, :polymorphic => true acts_as_double_polymorphic_join( :eaters => [:dogs, :cats], :eatens => [:cats, :birds] ) endNotice how Cats are both eaters and eatens. Since my Announcers and Announceables are entirely disparate, I was thinking the above didn’t apply to my situation. I’m mostly just confused…
Yeah, the examples are confusing — or, at least, demonstrate more features than most people are looking for.
Most people, like you, don’t need any of the children to play both polymorphic roles, but acts_as_double_polymorphic_join is appropriate for that too:
should fix you right up. (Note that that’s _all_ you need to say — the other associations are all autogenerated for you.)
(Ed: formatted code)
Thanks, Tom. That’s a very nice description about how it’s done.
It’ll be interesting to see what people come up with to help me DRY up the code in my next
has_many_polymorphspost!I’ve updated the post with The Right WayTM to do things. Tom, your code was a good start, but I did make a couple tweaks.