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.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Fenrir2

    Looks very good. Nice symmetry with create_table :).