DRY your Migrations

    Tim Lucas
    Share

    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.