Friday, April 14

has_many :gets even better

Update: My redefinition of << below is breaking normal has_many relationships. I haven't yet figured out why, but when I do I'll post a fix.

As I alluded to in the update to my last post, after it had stewed in my noggin for a couple hours, I realized that I had grossly overcomplicated two issues.

First of all, I didn't need to "cheat" the system by overriding the has_many signature and passing in the "hint" of the model being mapped to (Collectible). I can very easily check it at run time. I know the model of the relationship mapping (Possession), so I can check to see if the passed in object is an instance of the mapping (Possession) and if not, assume it's the model to be mapped to (Collectible).

That leads me to the second drastic improvement. I don't need to define new methods! I can override the existing has_many helper methods, and defer to their overridden counterparts when the mapping model (Possession) is passed in, or apply my new magic when the model to be mapped (Collectible) is passed in.

So first of all I beefed up the unit test with lots of commenty goodness and followed each test of the new overridden methods with a sanity check using the old [ugly] way:

def test_has_many_extensions
collector_id = 1
collectible_id = 3
collector = Collector.find( collector_id )
collectible = Collectible.find( collectible_id )
# ensure we are starting with a clean slate
assert ! collector.possessions.include?( collectible )
# sanity check -- compare it with the old-fashioned way
assert collector.possessions.find_by_collectible_id( collectible.id ).nil?
# add the collectible to the collector's possessions
collector.possessions << collectible
# ensure that the collectible was added to the collector's possessions
assert collector.possessions.include?( collectible )
# sanity check -- compare it with the old-fashioned way
assert ! collector.possessions.find_by_collectible_id( collectible.id ).nil?
# try to re-add it -- should this break?
collector.possessions << collectible
# reinitialize the collector so we can...
collector = Collector.find( collector_id )
# ...ensure that the collectible possession mapping was saved to the database
assert collector.possessions.include?( collectible )
# sanity check; compare it with the old-fashioned way
assert ! collector.possessions.find_by_collectible_id( collectible.id ).nil?
# remove the collectible from the collector's possessions
collector.possessions.delete( collectible )
# ensure that it was removed
assert ! collector.possessions.include?( collectible )
# sanity check; compare it with the old-fashioned way
assert collector.possessions.find_by_collectible_id( collectible.id ).nil?
# re-remove it -- should this break?
collector.possessions.delete( collectible )
# reinitialize the collector so we can...
collector = Collector.find( collector_id )
# ...ensure that the collectible possession mapping was removed from the database
assert ! collector.possessions.include?( collectible )
# sanity check; compare it with the old-fashioned way
assert collector.possessions.find_by_collectible_id( collectible.id ).nil?
end

Then I revered the Model declaration back to good old vanilla Rails syntax:

class Collector < ActiveRecord::Base

has_many :possessions

end

Then I completely scrapped the hack to override the has_many declaration and beefed up the AssociationCollection hack by overriding the actual old-school methods and deferring to their overridden counterparts when appropriate:

module ActiveRecord
module Associations
class AssociationCollection

def foreign_key( widget )
widget.class.to_s.downcase + '_id'
end

def find_relationship_wrapper( widget )
instance_eval "#{ @reflection.klass }.find_by_#{ foreign_key( widget ) }( #{ widget.id } )"
end

def include?( widget )
unless widget.instance_of?( @reflection.klass )
widget = find_relationship_wrapper( widget )
end
super( widget )
end

alias_method :old_delete, :delete

def delete( widget )
unless widget.instance_of?( @reflection.klass )
widget = find_relationship_wrapper( widget )
end
old_delete( widget )
end

def <<( widget )
if widget.instance_of?( @reflection.klass )
super( widget )
else
create( { foreign_key( widget ) => widget.id } )
end
end

end
end
end

Now this is a piece of code I can be proud of! Feast your eyes on that puppy and try not to drool on your keyboard ;-) Ha! I'm high on Ruby on Rails, and I ain't never comin' down!

No comments: