Tuesday, May 23

Automatic foreign keys in your migrations

A couple weeks back, Simon Harris blogged about a little plug-in he'd whipped up which makes your Ruby on Rails database migrations automatically guess and apply foreign key relationships. This was something I had been yearning for, so I jumped all over it. Unfortunately, it had a few problems:

- It didn't work with the Windows port of Ruby 1.8.4. Simon had developed and tested it on the OSX port of Ruby 1.8.4 and method aliasing -- for some reason -- isn't consistent between the two.

- It didn't work with MySQL. Simon had developed and tested it for PostgreSQL and the SQL syntax support differed slightly.

- It didn't handle column names that weren't clearly evident, so adhering to the Ruby on Rails philosophy, I recommended a little "configuration" to spice up the "convention".

- It choked on columns that looked like foreign keys but were not, attempting to create foreign keys to nonexistent tables.

- It didn't consistently properly pluralize table names.

Believe it or not, the set of migrations for my current pet project is complex enough to have exposed all of these issues.

So my correspondence with Simon graduated from comments on his blog to direct e-mails, and by last night we were lobbing chunks of code back and forth by the minute. We probably should have switched to some form of instant messaging, but we had our heads buried deep in the code. Due to the time difference (Simon is in Oslo at the moment, and I'm in South Florida) he was up until 1:30 in the morning (his time) working out the final kinks.

And by the grace of the Heavens, we finally got it perfect. My complex migrations run from beginning to end, perfectly applying every foreign key. This is my new favorite plug-in, and I'm proud to have helped it to fruition.

Simon has posted the latest and greatest version on RubyForge. Check it out!

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