Polymorphic has_many :through enhanced 1
Yesterday I posted an article describing how to do a DRY, readable polymorphic has_many :through join. Since then, I’ve made a few key improvements to them, so I thought I’d share them here.
The key shortcoming that came up as I used my has_interaction collection was that it wasn’t setting the interaction type automatically. This is a big problem, because the following code won’t work:
@user.hidden_messages << message
I guess my tests weren’t as clearly defined as they should have been. What actually happened was that the interaction was being created, but the interaction type stayed null. This makes sense, since I was only specifying it as a selection criteria. The tests only passed because the “hidden_messages” collection was being cached. So the first thing to do is to rewrite the tests so that they actually reload the data from the database to make sure the relationship has been persisted correctly. I’ve only included one test rewrite for brevity:
@dan.hidden_messages << @message_three
assert_equal @dan.hidden_messages(true).first, @message_three
end
This should apply to all the tests, though. The useful addition there is the (true) which forces the collection to reload from the database.
So how to make this work? Well, reading this good post by Josh Susser of the well-named “has_many :through” blog, I came up with this:
base.extend(ClassMethods)
end
has_many (args[:type] + "_" + args[:model].downcase.pluralize).to_sym,
:through => :interactions,
:source => :victim,
:source_type => args[:model],
:conditions => "interaction_type = ''" do
Interaction.with_scope(:create => {:interaction_type => args[:type] }) {self.concat victim }
end
end
end
end
end
That’s nice, and readable, and all that, but it doesn’t work. It throws a NoMethodError because args is not defined anymore by the time we executed “<<”. This makes sense, because args was a local variable to the has_interaction method, so it’s meaningless when we get back from our external code.
Unfortunately, Josh’s examples all involve only unextracted hasmany :through. If I’d written out each relationship explicitly without extracting it out, his code would have worked. However, I have extracted out hasinteractions, and it is more readable, so I’m going to keep it that way. How to get it to work, then? Well, on this front I got some very good help from the excellent #ruby-lang channel on freenode, mainly from 4 people with the handles “oGMo”, “Sepp2k”, “nutrimatt” and “apeiros”. The result was this:
base.extend(ClassMethods)
end
has_many (args[:type] + "_" + args[:model].downcase.pluralize).to_sym,
:through => :interactions,
:source => :victim,
:source_type => args[:model],
:conditions => "interaction_type = ''" do
class_eval do
define_method("<<") do |victim|
Interaction.with_scope(:create => {:interaction_type => args[:type] }) {self.concat victim }
end
end
end
end
end
end
I won’t go into a detailed explanation of why this works. If you got here looking for how to make it work, this works. If you want to find out why it works, a quick read through the api documentation for classeval and definemethod will clear it up.
There was one more useful improvement that apeiros suggested: The ClassMethods module I had copied from Rails was unnecessary. Instead of “include”ing, I could just “extend” the HasInteractions module and define the has_interactions in there directly. Here’s the resulting code:
has_many (args[:type] + "_" + args[:model].downcase.pluralize).to_sym,
:through => :interactions,
:source => :victim,
:source_type => args[:model],
:conditions => "interaction_type = ''" do
class_eval do
define_method("<<") do |victim|
Interaction.with_scope(:create => {:interaction_type => args[:type] }) {self.concat victim }
end
end
end
end
end
And in user.rb, we just replace “include HasInteractions” with “extend HasInteractions” - et voilà! It works. All the tests pass. It’s readable.
I hope you find this article useful and it helps you with what you’re trying to implement. If you have any further suggested improvements, or any comments, feel free to leave them below.
Trackbacks
Use the following link to trackback from your own site:
http://inter-sections.net/trackbacks?article_id=polymorphic-has_many-through-enhanced&day=26&month=09&year=2007
-
[...] How to be great Polymorphic has_many :through enhanced [...]




Meant to comment on this post… oh well you get a double thanks. ++++++++++++++++ Thanks… This actually was almost exactly what I was looking for. Made some modifications and implemented one of Josh Susser’s other methods “pushwithattributes” and it works great.
Thanks for posting!!!
-J