DBマイグレーション

マイグレーションは、時間を掛けてデータベーススキームを徐々に発展させていくActiveレコードの機能です。 純粋なSQLでスキーマの編集をするより、マイグレーションはRubyのDSL(ドメイン特化言語)にテーブルへの変更を書くことで 簡単にそれを行えるようにしてくれます。 このガイドを読むことで、次の事が学べるはずです。

  • ジェネレーターを使用して、マイグレーションファイルを作成する方法
  • Activeレコードが提供するメソッドで、データベースを操作する方法
  • Rakeタスクでマイグレーションとスキーマを操作する方法
  • マイグレーションがschema.rbと、どのように関連しているか

1. マイグレーションの概要

マイグレーションは、データベーススキーマを簡単に一貫した方法で、時間を掛けて変更していく便利な手法です。 RubyのDSLを使ってSQLを書くこと無く、データベースに依存しないでスキーマの変更をすることができます。

各マイグレーションで行ったことを、そのデータベースの"バージョン"として考えることが出来ます。 スキーマが何もない状態から始まり、各マイグレーションがテーブル、カラム、入力情報の追加・編集・削除を行います。 Activeレコードは、マイグレーションの履歴上のどの時点のものからでも、 スキーマを現在までのタイムラインに沿って最新のバージョンに更新することが出来ます。 Activeレコードはまた、データベースを最新の構造と同じものにするdb/schema.rbファイルを更新します。

下記はマイグレーションの例になります。

class CreateProducts < ActiveRecord::Migration
  def change
    create_table :products do |t|
      t.string :name
      t.text :description

      t.timestamps
    end
  end
end

このマイグレーションは、nameという文字列型のカラムとdescriptionというテキスト型のカラムを持つ、 productsというテーブルをを追加しています。 idという主キーが、Activeレコードモデルのデフォルトの主キーとして、暗黙的に追加されます。 timestampsマクロは、created_atupdated_atという2つのカラムを追加します。 これらの特殊なカラムは、Activeレコードによって自動的に管理されます。

状況が進行することで起こって欲しい変更を、自分達で定義することに注意してください。 このマイグレーションが実行される前はテーブルは存在せず、実行後にテーブルが存在することになります。 Activeレコードは、このマイグレーションを同様に戻す事も可能で、もしマイグレーションをロールバックすれば、 テーブルは削除されます。

トランザクションをサポートするデータベースであれば、スキーマによって状態を変更する際には、 マイグレーションはトランザクションにラップされます。 もし、データベースがトランザクションをサポートしない場合、マイグレーションが途中で失敗すると、 ロールバックする事が出来ません。 手動でロールバックを行う必要があります。

トランザクション内で実行出来ないクエリーが存在します。 もし、アダプターがDDLトランザクションをサポートする場合、 単一のマイグレーションでそれらを無効にするdisable_ddl_transaction!を使用することが可能です。

もし、Activeレコードが戻し方を知らない、何らかのマイグレーションを実行したい場合、reversibleを使用すること出来ます。

class ChangeProductsPrice < ActiveRecord::Migration
  def change
    reversible do |dir|
      change_table :products do |t|
        dir.up   { t.change :price, :string }
        dir.down { t.change :price, :integer }
      end
    end
  end
end

あるいは、代わりにupdownを使用することも出来ます。

class ChangeProductsPrice < ActiveRecord::Migration
  def up
    change_table :products do |t|
      t.change :price, :string
    end
  end

  def down
    change_table :products do |t|
      t.change :price, :integer
    end
  end
end

2. マイグレーションの作成

2.1 スタンドアローンなマイグレーションの作成

マイグレーションは、ファイルとしてdb/migrateディレクトリに格納され、それぞれがマイグレーションクラスになっています。 ファイルの名前は、YYYYMMDDHHMMSS_create_products.rbで、 UTCタイムスタンプ識別の後にアンダースコア区切りで、マイグレーションの名前が続いています。 マイグレーションクラスの名前(キャメルケース)は、ファイル名の後半部分と一致させる必要があります。 例えば、20080906120000_create_products.rbファイルのクラスはCreateProductsと定義し、 20080906120001_add_details_to_products.rbファイルのクラスはAddDetailsToProductsと定義すべきです。 Railsはマイグレーションを実行するべき順序を決定するのに、このタイムスタンプを使用します。 そのため、もしマイグレーションを他のアプリケーションからコピーしたり、自分でファイルを作成した際は、 この順序について注意する必要がります。

タイムスタンプをいちいち書くのは煩わしい作業のため、 Activeレコードはこのためのジェネレーターを提供してくれます。

$ rails generate migration AddPartNumberToProducts

中身は空ですが、適切なマイグレーションの名前のファイルが生成されます。

class AddPartNumberToProducts < ActiveRecord::Migration
  def change
  end
end

もし、下記のようにマイグレーション名を"AddXXXToYYY"、または"RemoveXXXFromYYY"の形式にし、 それに続いてカラム名と型を打ち込むと、マイグレーションはそれに合わせたadd_columnremove_columnが定義された ファイルを生成します。

$ rails generate migration AddPartNumberToProducts part_number:string

上記を打ち込むと、下記のファイルを生成します。

class AddPartNumberToProducts < ActiveRecord::Migration
  def change
    add_column :products, :part_number, :string
  end
end

もし、新しいカラムにindexを追加したい場合は、次のようにします。

$ rails generate migration AddPartNumberToProducts part_number:string:index

上記を打ち込むと、下記のファイルを生成します。

class AddPartNumberToProducts < ActiveRecord::Migration
  def change
    add_column :products, :part_number, :string
    add_index :products, :part_number
  end
end

同様にカラムを削除するマイグレーションも、コマンドラインから生成することが出来ます。

$ rails generate migration RemovePartNumberFromProducts part_number:string

上記を打ち込むと、下記のファイルを生成します。

class RemovePartNumberFromProducts < ActiveRecord::Migration
  def change
    remove_column :products, :part_number, :string
  end
end

カラムに1つに制限されているわけではありません。 例えば、

$ rails generate migration AddDetailsToProducts part_number:string price:decimal

上記を打ち込むと、下記のファイルを生成します。

class AddDetailsToProducts < ActiveRecord::Migration
  def change
    add_column :products, :part_number, :string
    add_column :products, :price, :decimal
  end
end

もし、マイグレーション名が"CreateXXX"の形式で、その後にカラム名と型を続けて打ち込むと、 マイグレーションはそのカラムを持つXXXテーブルを生成するファイルを生成します。 例えば、

$ rails generate migration CreateProducts name:string part_number:string

上記を打ち込むと、下記のファイルを生成します。

class CreateProducts < ActiveRecord::Migration
  def change
    create_table :products do |t|
      t.string :name
      t.string :part_number
    end
  end
end

いつものことですが、何が生成されているのかは出発点に過ぎません。 db/migrate/YYYYMMDDHHMMSS_add_details_to_products.rb fileを編集することで、 必要に応じて追加、削除を行うことが出来ます。

また、ジェネレーターは参照(belongs_toでも利用可能な)型のカラムも受け入れます。 例えば、

$ rails generate migration AddUserRefToProducts user:references

上記を打ち込むと、下記のファイルを生成します。

class AddUserRefToProducts < ActiveRecord::Migration
  def change
    add_reference :products, :user, index: true
  end
end

このマイグレーションは、user_idカラムと適切なインデックスを作成します。

名前の一部にJoinTableを含めることで、結合テーブルを作成することも出来ます。

rails g migration CreateJoinTableCustomerProduct customer product

上記を打ち込むと、下記のファイルを生成します。

class CreateJoinTableCustomerProduct < ActiveRecord::Migration
  def change
    create_join_table :customers, :products do |t|
      # t.index [:customer_id, :product_id]
      # t.index [:product_id, :customer_id]
    end
  end
end

2.2 モデルジェネレーター

モデルとスキャフォールドジェンレーターは、新しいモデルを追加するマイグレーションを作成します。 このマイグレーションは、っ常にテーブルと関連する構造を含むものになります。 もし、Railsに必要なカラムを伝えれば、カラムも追加された状態で生成されます。 例えば、下記を実行すると、

$ rails generate model Product name:string description:text

次のようなマイグレーションを作成します。

class CreateProducts < ActiveRecord::Migration
  def change
    create_table :products do |t|
      t.string :name
      t.text :description

      t.timestamps
    end
  end
end

また必要に応じて、名前/型のペアとしてカラムを追加することが可能です。

2.3 型の変更

また、型の後ろに中括弧({})で囲っていくつかのオプションを指定することが可能です。

limit
string/text/binary/integer型のカラムの最大サイズを設定します。
precision
decimal型の最大桁数(精度)を定義します。
scale
decimal型の小数点の右側にある数字の桁数(スケール)を定義します。
polymorphic
belongs_toと関連する型のカラムを追加します。

例えば、下記を実行すると

$ rails generate migration AddDetailsToProducts price:decimal{5,2} supplier:references{polymorphic}

次のようなファイルを生成します。

class AddDetailsToProducts < ActiveRecord::Migration
  def change
    add_column :products, :price, precision: 5, scale: 2
    add_reference :products, :user, polymorphic: true, index: true
  end
end

3. マイグレーションの編集

ジェネレーターを使用してマイグレーションを作成したので、このファイルを編集してみましょう。

3.1 テーブルの作成

create_tableメソッドは、基本的なものの1つですが、 モデルまたはスキャフォールドジェネレータにより、生成されていることがほとんどでしょう。 典型的な使い方は次のようになります。

create_table :products do |t|
  t.string :name
end

これはnameカラムを持つ(後述しますが、暗黙のidカラムも)、productsテーブルを作成します。

デフォルトでは、create_tableは、idという名前の主キーを生成します。 :primary_keyオプションで主キーの名前を変更することも可能です。 (紐づくモデルの変更も忘れないでください。) また、主キーが必要なければ、オプションにid: falseを渡してください。 もし、データベースに特定のオプションを指定する必要がある場合、:option内にSQLの断片を指定します。 例えば、

create_table :products, options: "ENGINE=BLACKHOLE" do |t|
  t.string :name, null: false
end

上記は、テーブル作成時にENGINE=BLACKHOLEをSQLに追記します。 (MYSQL使用時は、デフォルトはENGINE=InnoDBになります)

3.2 結合テーブルの作成

マイグレーションメソッドのcreate_join_tableは、 HABTM(Has And Belongs To Many)の結合テーブルを作成します。 典型的な使い方は下記のようになります。

create_join_table :products, :categories

これは、category_idとproduct_idと呼ばれる2つのカラムを持つcategories_productsテーブルを作成します。 これらのカラムは、デフォルトで:nullオプションにfalseが設定されます。

:table_nameオプションを渡して、テーブル名をカスタマイズすることが可能です。 例えば、

create_join_table :products, :categories, table_name: :categorization

とすると、categorizationテーブルを作成します。

デフォルトでは、create_join_tableはオプション無しの2つのカラムを作成しますが、 :column_optionsオプションを使用することで、指定することが出来ます。 例えば、

create_join_table :products, :categories, column_options: {null: true}

とすると、:nullオプションがtrueのproduct_idとcategory_idが生成されます。

create_join_tableは、ブロックの受け取りも可能なので、 下記のようにして、複数のindex(デフォルトでは作成されません)や、カラムを追加することが出来ます。

create_join_table :products, :categories do |t|
  t.index :product_id
  t.index :category_id
end

3.3 テーブルの変更

create_tableに近しいものに、既存のテーブルを変更するchange_tableがあります。 create_table同様の方法で使用されますが、ブロック内でより多くの事を行うことが出来ます。 例えば、

change_table :products do |t|
  t.remove :description, :name
  t.string :part_number
  t.index :part_number
  t.rename :upccode, :upc_code
end

descriptionとnameカラムを削除し、 文字列型のpart_numberカラムを追加し、それにインデックスを付けています。 最後にupcdoeカラムの名前を変更しています。

3.4 ヘルパーだけでは十分ではない場合

もし、Activeレコードが提供するヘルパーだけでは不十分な場合、 executeを使用することで、任意のSQLを実行することが出来ます。

Products.connection.execute('UPDATE `products` SET `price`=`free` WHERE 1')

個別のメソッドのより詳しい内容や例を参照したければ、APIドキュメントを参照してください。

ActiveRecord::ConnectionAdapters::SchemaStatements
change、up、downメソッドで利用可能なメソッドを提供
ActiveRecord::ConnectionAdapters::TableDefinition
create_tableで利用可能なメソッドを提供
ActiveRecord::ConnectionAdapters::Table
change_tableで利用可能なメソッドを提供

3.5 changeメソッドの使用

changeメソッドは、よく使用されるマイグレーションです。 ありがちなケースで、Activeレコードはマイグレーションを自動的に元に戻すことが出来ます。 現在のところ、changeメソッドは、下記のマイグレーション定義をサポートしています。

  • add_column
  • add_index
  • add_reference
  • add_timestamps
  • create_table
  • create_join_table
  • drop_table (ブロックのみ)
  • drop_join_table (ブロックのみ)
  • remove_timestamps
  • rename_column
  • rename_index
  • remove_reference
  • rename_table

ブロックで、changechange_defaultremoveを呼び出さない限り可逆的です。

もし、他のメソッドを使用する必要がある場合、 reversibleを使用するか、またはupdownメソッドを、 changeメソッドの代わりに使用すべきです。

3.6 reversibleの使用

複雑なマイグレーションですと、Activeレコードでは元に戻すことが出来ないかもしれません。 元に戻すための実行で何をするかを、reversibleを使用して指定することが出来ます。 例えば、

class ExampleMigration < ActiveRecord::Migration
  def change
    create_table :products do |t|
      t.references :category
    end

    reversible do |dir|
      dir.up do
        #add a foreign key
        execute <<-SQL
          ALTER TABLE products
            ADD CONSTRAINT fk_products_categories
            FOREIGN KEY (category_id)
            REFERENCES categories(id)
        SQL
      end
      dir.down do
        execute <<-SQL
          ALTER TABLE products
            DROP FOREIGN KEY fk_products_categories
        SQL
      end
    end

    add_column :users, :home_page_url, :string
    rename_column :users, :email, :email_address
  end

reversibleは、構造を実行される正しい順番を確約してくれます。 もし、前の例のマイグレーションが戻されたのであれば、 downブロックは、home_page_urlが削除された後、 productテーブルがドロップされる直前に実行されます。

時に、例えばデータの削除など、マイグレーションで元に戻せないものがあります。 そのようなケースでは、downブロック内でActiveRecord::IrreversibleMigrationを発生させることが出来ます。 もし、誰かがマイグレーションを戻そうと試みた場合、「it can't be done.」という、エラーメッセージが表示されます。

3.7 up/down メソッドの使用

changeメソッドの代わりに、マイグレーションの古いスタイルであるupdownを使用することも出来ます。 upメソッドには、スキーマに対して行いたい変更を記述し、 downメソッドはupメソッドによる変更を元に戻す記述を書くべきです。 言い換えるならば、もしupdownを続けて実行したのであれば、データベーススキーマは不変であるべきです。 例えば、もしupメソッド内でテーブルを作成する場合、downメソッド内でテーブルをドロップすべきです。 元に戻すための変更は、upメソッドによって実行された変更の反対の順番で実行するのが賢明です。 下記は、reversible句と同等です。

class ExampleMigration < ActiveRecord::Migration
  def up
    create_table :products do |t|
      t.references :category
    end

    # add a foreign key
    execute <<-SQL
      ALTER TABLE products
        ADD CONSTRAINT fk_products_categories
        FOREIGN KEY (category_id)
        REFERENCES categories(id)
    SQL

    add_column :users, :home_page_url, :string
    rename_column :users, :email, :email_address
  end

  def down
    rename_column :users, :email_address, :email
    remove_column :users, :home_page_url

    execute <<-SQL
      ALTER TABLE products
        DROP FOREIGN KEY fk_products_categories
    SQL

    drop_table :products
  end
end

もし、マイグレーションが不可逆的なものであれば、downメソッド内でActiveRecord::IrreversibleMigrationを発生させるべきです。 誰かがマイグレーションを戻そうとすると、エラーメッセージとして「it can't be done.」が表示されます。

3.8 前のマイグレーションに戻す

revertメソッドを使用することで、Activeレコードのロールバックマイグレーションの機能を使用することが出来ます。

require_relative '2012121212_example_migration'

class FixupExampleMigration < ActiveRecord::Migration
  def change
    revert ExampleMigration

    create_table(:apples) do |t|
      t.string :variety
    end
  end
end

また、revertメソッドは、元に戻すための指示が書かれたブロックを受け入れます。 これは、前のマイグレーションの一部を選択して元に戻す際に便利です。 例えば、前述のExampleMigrationが既にコミットされており、 後ほどProductリストを、代わりにシリアライズすることが最良だと判断したようなケースを想像してください。 シリアライズのためのマイグレーションを下記のように書くことが出来ます。

class SerializeProductListMigration < ActiveRecord::Migration
  def change
    add_column :categories, :product_list

    reversible do |dir|
      dir.up do
        # Category#product_listによるProductsから、データを移すコード
      end
      dir.down do
        # Category#product_listからProductsを作成
      end
    end

    revert do
      # ExampleMigrationからコピー&ペースト
      create_table :products do |t|
        t.references :category
      end

      reversible do |dir|
        dir.up do
          #add a foreign key
          execute <<-SQL
            ALTER TABLE products
              ADD CONSTRAINT fk_products_categories
              FOREIGN KEY (category_id)
              REFERENCES categories(id)
          SQL
        end
        dir.down do
          execute <<-SQL
            ALTER TABLE products
              DROP FOREIGN KEY fk_products_categories
          SQL
        end
      end

      # The rest of the migration was ok
    end
  end
end

同じマイグレーションをrevertを使わずに書くことも可能ですが、 その場合は更にいくつかのステップを踏むことが必要になります。 create_tablereversibleを順番に元に戻し、 create_tabledrop_tableに入れ替え、 最後にupdownに入れ替え、逆の場合も同等です。

4. マイグレーションの実行

Railsは、マイグレーションのセットを実行させるRakeタスクのセットを提供します。

最初にマイグレーションで使用することになるRakeタスクは、おそらくrake db:migrateになるでしょう。 これは最も基本的なフォームで、まだ実行されていない全てのマイグレーションのchange、またはupメソッドを実行します。 もし、そのようなマイグレーションが無ければ、何もせずに終了します。 マイグレーションの日付を元に、その日付順でマイグレーションは実行されます。

db:migrateはまた、db:schema:dumpも実行ことに注意してください。 これは、db/schema.rbファイルをデータベースの構造にあった状態に更新します。

もし、バージョンを指定した場合、Activeレコードはその指定したバージョンに達するまでの必要なマイグレーション(change、up、down)を実行します。 バージョンは、マイグレーションファイルの数値の接頭辞(プレフィックス)になります。 例えば、バージョン20080906120000の指定をして実行するのであれば、下記のようにして実行します。

$ rake db:migrate VERSION=20080906120000

もし、バージョン20080906120000が現在のバージョンより上のバージョンで、マイグレーションが進む状態である場合、 20080906120000バージョンを含む全てのマイグレーションを上げる、change(またはup)メソッドが実行され、 それより後のマイグレーションは実行されません。 もし、マイグレーションが戻る状態である場合、20080906120000バージョンを含まないマイグレーションを下げる全てのdownが実行されます。

4.1 ロールバック

一般的なタスクは、最後に実行したマイグレーションをロールバックすることができます。 例えば、ミスをしてこれを修正したいと考えているとします。 前のマイグレーションを辿ってバージョンを戻すマイグレーションを作るのではなく、下記を実行することでロールバックすることが出来ます。

$ rake db:rollback

これは直近のマイグレーションを、changeメソッド、またはdownメソッドによって元に戻します。 もし、いくつかのマイグレーションを戻す必要がある場合は、STEPパラメーターを使用してそれを行うことが可能です。

$ rake db:rollback STEP=3

これは直近の3つのマイグレーションを戻します。

db:migrate:redoタスクは、ロールバックをし、再度そのマイグレーションを実行する処理のショートカットです。 db:rollbackタスクとして、1つ以上のバージョンを戻す必要がある場合は、下記のようにしてSTEPパラメーターを使用することが可能です。

$ rake db:migrate:redo STEP=3

これらのRakeタスクはどれも、db:migrateを使って出来ないことを行えるわけではありません。 マイグレーション時にバージョンを明確に指定する必要がないため、単に便利であるという事に過ぎません。

4.2 データベースのリセット

rake db:resetタスクは、データベースをドロップ、再構築し、最新のスキーマを読み込みます。

これは全てのマイグレーションを実行する事とは異なります。 現在のschema.rbファイルの内容を使用しているに過ぎません。 もし、マイグレーションがロールバック出来ない場合は、rake db:resetが有効かもしれません。 更に詳しく知りたければ、このページの'schema dumping and you.'を参照してください。

4.3 特定のマイグレーションの実行

もし、特定のup、またはdownマイグレーションを実行する必要がある場合、 db:migrate:updb:migrate:downでそれを行います。 適切なバージョンを指定するだけで、一致するマイグレーションのchangeup、 またはdownが実行されます。例えば、

$ rake db:migrate:up VERSION=20080906120000

は、20080906120000マイグレーションが,changeメソッド(または、upメソッド)によって実行されます。 このタスクはまず、マイグレーションが既に実行されているかどうかを調べ、もしActiveレコードが既に実行されていると判定すれば何もしません。

4.4 異なる環境下でのマイグレーションの実行

デフォルトではrake db:migrateは開発環境下で実行されます。 他の環境に対してマイグレーションを実行するには、コマンド実行時にRAILS_ENVを使用して環境変数を指定します。 例えば、test環境でマイグレーションを実行したければ、下記のようにします。

$ rake db:migrate RAILS_ENV=test

4.5 マイグレーション実行出力の変更

デフォルトでマイグレーションは、何をしたか、どれぐらい時間が掛かったかを正確に教えてくれます。 マイグレーションは、テーブルを作成し、このような出力を行います。

==  CreateProducts: migrating ======================
-- create_table(:products)
   -> 0.0028s
==  CreateProducts: migrated (0.0028s) =============

開発者が好きなように制御することが出来るメソッドが、マイグレーションから提供されています。

メソッド 目的
suppress_messages 引数としてブロックを取り、ブロックによって生成された出力を抑制します。
say 引数をメッセージとして出力します。 2つ目の真偽値の引数には、インデントをするか否かの指定します。
say_with_time ブロックを実行するのに掛かった時間をテキストで出力します。 ブロックが整数を返す場合、それは影響を受けた行数であるとみなされます。

例えば、

class CreateProducts < ActiveRecord::Migration
  def change
    suppress_messages do
      create_table :products do |t|
        t.string :name
        t.text :description
        t.timestamps
      end
    end

    say "Created a table"

    suppress_messages {add_index :products, :name}
    say "and an index!", true

    say_with_time 'Waiting for a while' do
      sleep 10
      250
    end
  end
end

次のように出力されます。

==  CreateProducts: migrating ===========================
-- Created a table
   -> and an index!
-- Waiting for a while
   -> 10.0013s
   -> 250 rows
==  CreateProducts: migrated (10.0054s) =================

もし、Activeレコードに何も出力させたくなければ、 rake db:migrate VERBOSE=falseを実行すれば、全ての出力が表示されなくなります。

5. 既に存在するマイグレーションの変更

時折、間違ったマイグレーションを書いてしまうことがあるかもしれません。 もし、既にそのマイグレーションを実行してしまうと、そのマイグレーションを編集して、再度マイグレーションを実行することが出来ません。 Railsは既にマイグレーションを実行したとみなし、rake db:migrateを実行しても何もしません。 そういった場合、マイグレーションをロールバック(例えば、rake db:rollbackを使用して)し、 マイグレーションを修正して、正しいものをrake db:migrateで実行すべきです。

通常、既存のマイグレーションを編集することはお勧めできません。 もし、既存のバージョンのマイグレーションがproduction環境上で実行されていた場合、 余計な仕事と頭痛の種が増えることになるでしょう。 代わりに、その問題を修正するマイグレーションを書くべきです。 新しく生成したマイグレーションを、適用する前(より分かりやすく言うと、他の開発マシンに広がる前)に編集することについては、 それほど害はないはずです。

revertメソッドは、新しいマイグレーションを前のマイグレーションに、一部または全てを戻す際に便利です。 (上述の、「3.8 前のマイグレーションに戻す」を参照してください。)

6. マイグレーションでのモデルの使用

データの作成、または更新をマイグレーションで行う際に、自分で定義したモデルを使用したくなるかもしれません。 Railsは、それを基盤としてデータに簡単にアクセスするための機能を提供してくれています。 ただし、それを行うにはいくつか注意すべき項目があります。

例えば、その時点ではデータベース上に存在しないカラムを、(1)その時点で、または(2)それに続くマイグレーション時に、 モデルが作成しようとした場合に問題が発生します。

以降のの例で、どういう事か分かりやすく説明します。
アリスとボブがProductモデルが含まれる同じコードを共有して作業していたとします。

ボブが休暇を取りました。

アリスはproductsテーブルに新しいカラムを追加し、それを初期化するマイグレーションを作りました。 また、Prodcutモデルのその新しいカラムに対して、検証(Validation)処理も追加しました。

# db/migrate/20100513121110_add_flag_to_product.rb

class AddFlagToProduct < ActiveRecord::Migration
  def change
    add_column :products, :flag, :boolean
    reversible do |dir|
      dir.up { Product.update_all flag: false }
    end
    Product.update_all flag: false
  end
end
# app/models/product.rb

class Product < ActiveRecord::Base
  validates :flag, presence: true
end

アリスは次に、productsテーブルに先程とは異なるカラムを追加し、それを初期化するためのマイグレーションを追加しました。 Productモデルには、その新しいカラムに対しての検証(Validation)処理も追加しました。

# db/migrate/20100515121110_add_fuzz_to_product.rb

class AddFuzzToProduct < ActiveRecord::Migration
  def change
    add_column :products, :fuzz, :string
    reversible do |dir|
      dir.up { Product.update_all fuzz: 'fuzzy' }
    end
  end
end
# app/models/product.rb

class Product < ActiveRecord::Base
  validates :flag, :fuzz, presence: true
end

アリスの作業環境で、両方のマイグレーションが適用されました。

ボブが休暇から帰ってきました。そして…、

  • 両方のマイグレーションと最新のバージョンのProductモデルが含まれるソースコードに更新
  • 更新されたProductモデルが含まれた状態で、rake db:migrateによるマイグレーションを実行

ここで、マイグレーションの実行に失敗します。 何故なら、モデルが保存を試みた際に、最初のマイグレーション実行時点では、2番目に追加されるはずのカラム(fuzz)はその時点ではデータベース上に存在しないのに、 2番目に追加されたカラム(fuzz)の検証をしようとしてしまうからです。

rake aborted!
An error has occurred, this and all later migrations canceled:

undefined method `fuzz' for #<Product:0x000001049b14a0>

解決するには、マイグレーションを考慮したモデルをローカル環境で作成し、 Railsが検証をパスしてマイグレーションの実行を完了できるようにします。

ローカルのモデルを使用する際に、Product.reset_column_informationを呼んで、 データベースのデータ更新の前に、ProductモデルのためにActiveレコードのキャッシュをクリアしておくと良いでしょう。

もし、アリスが下記のようにしていれば、問題は発生しなかったでしょう。

# db/migrate/20100513121110_add_flag_to_product.rb

class AddFlagToProduct < ActiveRecord::Migration
  class Product < ActiveRecord::Base
  end

  def change
    add_column :products, :flag, :boolean
    Product.reset_column_information
    reversible do |dir|
      dir.up { Product.update_all flag: false }
    end
  end
end
# db/migrate/20100515121110_add_fuzz_to_product.rb

class AddFuzzToProduct < ActiveRecord::Migration
  class Product < ActiveRecord::Base
  end

  def change
    add_column :products, :fuzz, :string
    Product.reset_column_information
    reversible do |dir|
      dir.up { Product.update_all fuzz: 'fuzzy' }
    end
  end
end

上記の例とは別の更に悪いケースがありえます。

例えば、アリスが特定のdescriptionフィールドを更新するマイグレーションを作成したとします。 彼女はマイグレーションを実行して、そのコードをコミットし、新しい機能のためにproductテーブルにfuzzカラムを追加する作業を始めます。

彼女はその新しい機能のために2つのマイグレーションを作成します。 1つ目は新しいカラムの追加、2つ目は他のproductの属性を元に、特定のfuzzカラムを更新します。

このマイグレーションの実行では特に問題は起こりませんが、 ボグが休暇から帰ってきてrake db:migrateによって未実行の全てのマイグレーションを実行すると、 巧妙なバグが紛れ込んでしまいます。 descriptionは初期化し、fuzzカラムが提供されますが、fuzzは全てnilになります。

この解決策も、マイグレーションがProductモデルを参照する前に、Product.reset_column_informationを使用して、 レコードのデータが操作される前にActiveレコードが覚えるべき現在のテーブル構造を確立することです。

7. スキーマのDump

7.1 schemaファイルは何のためにあるのか?

マイグレーションは、DBスキーマとして信頼できるソースではありません。 その役割を担うのは、db/schema.rb、もしくはデータベースを調べてActiveレコードによって生成されたSQLファイルです。 それらは現在のデータベースの状態を表したもので、編集されるように設計されていません。

アプリケーションを再構築する際に、全てのマイグレーションの履歴を辿ったデプロイをする必要はありません。(しかも、エラーになる傾向があります) 現在のスキーマの記述を読み込んむ事で、よりシンプルに素早くそれを行うことが出来ます。

例えば、これはtestのデータベースを作成する方法ですが、 現在のdevelopmentデータベースをダンプし、(db/schema.rbまたは、db/structure.sql) それをtestデータベースに読み込みます。

またschemaファイルは、Activeレコードオブジェクトが何の属性を持つか素早く確認したいような場合にも便利です。 この情報はモデルのコードではなく、複数のマイグレーションの情報にまたがって構成されていますが、 schemaファイルに上手に要約されています。 annotate_modelsgemは、 その機能を希望すれば、自動で各モデルの上部にスキーマの要約のコメントの追加と更新を行います。

7.2 Schema Dumpsのタイプ

スキーマのdumpには2つの方法があります。 これは、config/application.rb内のconfig.active_record.schema_formatに、 :sqlまたは:rubyを指定することで設定されます。

:rubyが選択された場合、スキーマ情報はdb/schema.rbに格納されます。 このファイルを参照すると、かなりボリュームのあるマイグレーションファイルであることに驚くかもしれません。

ActiveRecord::Schema.define(version: 20080906171750) do
  create_table "authors", force: true do |t|
    t.string   "name"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  create_table "products", force: true do |t|
    t.string   "name"
    t.text "description"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.string "part_number"
  end
end

このファイルはデータベースを調べ、それを元にcreate_tableadd_indexで構造を表現します。 こうすることでデータベースの種類に依存しなくなり、Activeレコードのサポートによってデータベースに読み込むことが可能になります。 これは、複数のデータベースで使用可能なものとして、アプリケーションを配布する際に非常に便利です。

しかし、db/schema.rbは外部キー制約、トリガー、ストアド・プロシージャなどのデータベース固有のものは表現出来ないため、それとのトレードオフになります。 また、DB固有のSQLを直接実効するようなマイグレーションがある限り、スキーマのdumpによってデータベースを再構築することは不可能になります。 もし、今挙げたようなことを行いたいのであれば、:sqlフォーマットを使用すべきです。

Activeレコードのスキーマのdumpの代わりにそのデータベースの構造を、 データベース固有のツールを使用して(db:structure:dumpのRakeタスクを通して)db/structure.sqlにdumpします。 例えば、PostgreSQLであればpg_dumpユーティリティが使用されます。 MySQLであれば、SHOW CREATE TABLEによる出力が含まれす。

これらのスキーマを読み込むことに関して、これに含まれるSQL実行文がどうなるかという疑問があると思います。 定義上は、データベース構造の完全なコピーを作成します。 スキーマフォーマットに:sqlを指定した場合、それが作成されることに使用されないものに関しては、 RDBMSへの読み込みをしないようにします。(翻訳に自信無し)

7.3 スキーマのdumpとソースの制御

スキーマがdumpするものは、データベースの構造として信頼のおけるソースであるため、 そのソースがどのようなものであるか確認することを強くお勧めします。

8. Activeレコードと参照整合性

Activeレコードはデータベースではなく、モデルに従って処理を行います。 そのため、トリガーや外部キー制約のような機能は頻繁に使用されません。(翻訳に自信無し)

validates :foreign_keyuniqueness: trueのような検証は、データの正当性を保持するための1つの方法です。 関連性を表す:dependentオプションは、親のデータに基づく子データを、親データが削除された際に自動的に削除します。 これらはアプリケーション層の制御であるため、完全なデータを保証することが出来ず、 そのため一部の人々は、データベースの外部キー制約でそれを補おうとします。

Activeレコードは、そのような機能を備えたツールを提供しませんが、 executeメソッドは、任意のSQLを実行するのに使用することが出来ます。 また、foreignerのようなgemを使用して、 Activeレコードに外部キーサポート(db/schema.rbへの外部キーサポートも含みます)を追加することも可能です。

9. マイグレーションとSeedデータ

一部の々は、データの追加にマイグレーションを使用します。

class AddInitialProducts < ActiveRecord::Migration
  def up
    5.times do |i|
      Product.create(name: "Product ##{i}", description: "A product.")
    end
  end

  def down
    Product.delete_all
  end
end

ただし、Railsは'seeds'と呼ばれる初期データを作成するための機能を持っています。 これはとてもシンプルな機能で、db/seeds.rbにRubyコードで処理を下記、rake db:seedで実行します。

5.times do |i|
  Product.create(name: "Product ##{i}", description: "A product.")
end

空のアプリケーションのデータベースのセットアップに、よく使用されます。

 Back to top

© 2010 - 2017 STUDIO KINGDOM