Tuesday, May 2

has_many :helpers, recapped

Based on comments to my last post it's become apparent that readers haven't been able to follow my piecemeal posts and assemble them back into a coherent big picture, and that makes perfect sense as I don't do that for the blogs I read either, so I need to start composing my posts as stand-alone self-contained nuggets, providing context and back-story where appropriate. So with that in mind, I'm composing here a wrap-up state-of-the-union essay on my has_many helper plug-in.

First of all, I'll revisit why I created this helper utility. I have Collectors. I have Collectibles. Collectors possess Collectibles, modeled as Possessions. It looks like this in code:

class Collector < ActiveRecord::Base
has_many :possessions
end

class Possession < ActiveRecord::Base
belongs_to :collector
belongs_to :collectible
end

class Collectible < ActiveRecord::Base
end

Note that I'm not using has_and_belongs_to_many and I'm not using the "through" variation.

Now what I want to do is be able to add Collectibles to a Collector's Possessions like so:

collector = Collector.find( 123 )
collectible = Collectible.find( 456 )
collector.possessions << collectible

But, of course, that doesn't work with Rails. I will get the error:

ActiveRecord::AssociationTypeMismatch: Possession expected, got Collectible

Rails expects me to code it this way:

collector.possessions.create( :collectible_id => collectible.id )

I say pshaw to that, good sir! And so I decided to fix it.

First, like a good little Rails programmer, I adhered to the religion of Test-Driven Development and wrote a test for the way I thought it should work:

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 made it work with the following plug-in, which is available on RubyForge at http://rubyforge.org/projects/tedshasmanyhelp/:

module ActiveRecord
module Associations
class AssociationCollectible < AssociationProxy

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

def teds_find_relationship_wrapper( widget )
conditions = "#{ teds_guess_foreign_key( widget ) } = #{ widget.id }"
find( :first, :conditions => conditions )
end

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

alias_method :old_delete, :delete

def delete( widget )
unless widget.instance_of?( @reflection.klass )
widget = teds_find_relationship_wrapper( widget )
end
unless widget.nil? # I should probably raise an error
old_delete( widget )
end
end

alias_method :old_push, '<<'

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

end
end
end

No comments: