Conditional Logic in Rails Migrations

Migrations are one of the most useful features provided by the Rails framework. A migration is a class implemented in the Ruby programming language that automates the performance of one or more database actions, such as creating a table, dropping a table, altering a table, creating an index, etc. More specifically, a migration provides an "up" method and a "down" method, where the "up" method performs a database action and the "down" method undoes the action.

The Rails framework also provides migrations with a database-agnostic syntax for performing database-related actions. In general, migrations easily and effectively work across a variety of databases, with the Rails framework "doing the right thing" most of the time for each type of database.

A sample migration, "002_create_users.rb" is shown below:

Listing 1 - A Sample Migration

class CreateUsers < ActiveRecord::Migration

   def self.up
      create_table :co_users, :primary_key => :user_id do |t|
         # t.column :name, :string
t.column :first_name, :string, :null=>false, :limit=>50 t.column :last_name, :string, :null=>false, :limit=>50 t.column :user_name, :string, :null=>false, :limit=>50 t.column :password, :string, :null=>true, :limit=>40 t.column :email_address, :string, :null=>false, :limit=>100 t.column :active_ind, :boolean, :null=>false, :default=>true t.column :created_at, :timestamp, :null=>false t.column :updated_at, :timestamp, :null=>false end end def self.down drop_table :co_users end end

Pretty simple.

In practice, most projects include numerous migrations. Each migration is numbered, so they can be easily executed in the proper order to create a database ... or to tear it back down. What this means is that the common task of creating development or test versions of any project's database becomes a virtually trivial activity.

The astute reader will hereby note that having database-agnostic migrations that do the right thing "most of the time" is not the same thing as "all the time." It's common for developers to work with one database during development (usually MySQL) while ultimately fielding the project on the customer's target database of choice, such as Oracle.

There can be a few issues when actively using migrations to support multiple types of databases. Some of the issues can require conditional actions, such as performing one action in MySQL but something different in Oracle.

For the sake of this article, we're going to define an issue that requires conditional action within a migration. So, here's our issue. In production, which will be an Oracle database, the CO_USERS table is a legacy table and cannot, under any circumstances, be dropped without seriously impacting other production applications. However, MySQL is used as the database of choice in development and test environments.

The migration needs to create and drop the table when the database is MySQL, but should skip these steps when the database is Oracle. Here's a revised migration that has the necessary logic:

Listing 2 - A Migration With Conditional Logic

class CreateUsers < ActiveRecord::Migration

  def self.up
    # Gets the database adapter info defined in the database.yml file
    adapter = User.connection.instance_variable_get("@config")[:adapter]
    puts("Database Adapter Detected: " + adapter)

    if adapter == "mysql"
       create_table :rm_users, :primary_key => :user_id do |t|
          # t.column :name, :string
          t.column :first_name, :string, :null=>false, :limit=>50
          t.column :last_name, :string, :null=>false, :limit=>50
          t.column :user_name, :string, :null=>false, :limit=>50
          t.column :password, :string, :null=>true, :limit=>40
          t.column :email_address, :string, :null=>false, :limit=>100
          t.column :active_ind, :boolean, :null=>false, :default=>true
          t.column :created_at, :timestamp, :null=>false
          t.column :updated_at, :timestamp, :null=>false
       end

       CreateUsers.create_dev_users
    end
 end

 def self.down
    adapter = User.connection.instance_variable_get("@config")[:adapter]
    puts("Database Adapter Detected: " + adapter)

    if adapter == "mysql"
       drop_table :rm_users
    end
 end

 private

 # Create user accounts for use in Dev/Testing environments. In the
 # future, we probably ought to load this data from a fixture.

 def self.create_dev_users
    # Create a default user (with a pre-encrypted password)
    User.create :screen_name=>"root",
       :password=>"a87ce080f4064d265776667d9cd757db01797c32",
       :salt=>"544384000.640786778811626",
       :first_name=>"user",
       :last_name=>"root",
       :email_address=>"fake_email@aol.com"
 end

end

In the revised migration, the table is only created or dropped if the database is MySQL. If the database is Oracle, the migration is successfully executed but does not perform any database actions. The key code that determines the type of database being used is:

      adapter = User.connection.instance_variable_get("@config")[:adapter]

This code interrogates the database connection for that type of class to determine the type of database adapter being used. The code can then make decisions about what steps to perform based on this information.

Note that you could also do this with:

      adapter = ActiveRecord::Base.connection.instance_variable_get("@config")[:adapter]

However, that statement uses the default database connection for the environment. An application that hits multiple databases will generally have models defined that establish their own database connections rather than using the default. In the real-life application on which this article was based, the application hit two databases in production, Oracle and Sybase, with MySQL used only for development.

The basic way of getting the connection information is the same in both techniques. Choose the one that's appropriate for your application.

Additionally, it seemed useful to create a default account if the database was MySQL; this way, developers will start out with a usable user account in their development environment.

While it's useful to keep migrations as database-agnostic as possible, it's not always practical. As this example has illustrated, it is both possible and sometimes extremely useful to be able to perform conditional actions within a migration.



Comments

No comments yet. Be the first.



Leave a Comment

Comments are moderated and will not appear on the site until reviewed.

(not displayed)