DRY your Migrations
Rails 1.1 introduced Object#with_options
which allows you to remove duplication for method calls with common options, but wouldn’t it be nice if we could use this in migrations too?
For example, the following routing code:
map.connect 'atom.xml', :controller => 'feed', :action => 'everything_atom'
map.connect 'comments-atom.xml', :controller => 'feed', :action => 'comments_atom'
can use the Object#with_options
magic to read:
map.with_options(:controller => 'feed') do |feed|
feed.connect 'atom.xml', :action => 'everything_atom'
feed.connect 'comments-atom.xml', :action => 'comments_atom'
end
Nice, eh?
Let’s have a look at a migration I wrote recently:
class AddExcerptSlugAndImageToArticles < ActiveRecord::Migration
def self.up
add_column :articles, :excerpt, :text
add_column :articles, :slug, :string
add_column :articles, :image, :string
end
def self.down
remove_column :articles, :excerpt
remove_column :articles, :slug
remove_column :articles, :image
end
end
Argh… articles, articles everywhere! Firstly, let’s come up with how we’d like the API to look:
class AddExcerptSlugAndImageToArticles < ActiveRecord::Migration
def self.up
with_table :articles do |t|
t.add_column :excerpt, :text
t.add_column :slug, :string
t.add_column :image, :string
end
end
def self.down
with_table :articles do |t|
t.remove_column :excerpt
t.remove_column :slug
t.remove_column :image
end
end
end
Ahh… much better. How do we go about adding this with_table
method? Firstly, we need to add the the with_table
method to the ActiveRecord::Migration
class:
class ActiveRecord::Migration
def self.with_table(table_name)
# Funky magic
end
end
The with_table
method will need to yield
an object which acts just like the migration but adds the table name to the front of the argument list. The simplest way to do this is to create a new Object
which we’ll use as a proxy. We’ll define the proxy’s method_missing
method from which we’ll call the original migration, but with the addition of the table name to the front of the argument list. If we call the proxy with add_column :blurb, :string
it will call the original migration with add_column :articles, :blurb, :string
.
Putting all this together, we end up with:
class ActiveRecord::Migration
def self.with_table(table_name)
proxy = Object.new
proxy.instance_variable_set(:@migration, self)
proxy.instance_variable_set(:@table_name, table_name)
def proxy.method_missing(method_name, *args)
@migration.send(method_name, *(args.to_a.insert(0, @table_name)))
end
yield proxy
end
end
Nice eh? Try chucking this into the top of one of your migration files and testing it out, and if you want to read more about this method_missing madness check out Chapter 6 of Why’s Poignant Guide to Ruby.