Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails API: ActiveRecord::Migration(翻訳)

概要

MITライセンスに基づいて翻訳・公開いたします。

Rails API: ActiveRecord::Migration(翻訳)

複数の物理データベースで使われるスキーマの成長をマイグレーションで管理できます。マイグレーションは、新しい機能を使うためにローカルデータベースにフィールドを追加するときによく起こる問題を解決する方法のひとつですが、変更点を他の開発者やproductionサーバーにプッシュする方法までは関知しません。マイグレーションを用いることで、自己完結するクラスの変換方法を(Gitなどの)バージョン管理システムにチェックインして記述し、別のデータベースに対して1バージョン前、2バージョン前、5バージョン前のものを実行できるようになります。

シンプルなマイグレーションの例を以下に示します。

class AddSsl < ActiveRecord::Migration[5.0]
  def up
    add_column :accounts, :ssl_enabled, :boolean, default: true
  end

  def down
    remove_column :accounts, :ssl_enabled
  end
end

上のマイグレーションは、accountsテーブルに:booleanフラグを追加し、マイグレーションが巻き戻されるときには削除します。また、あらゆるマイグレーションでマイグレーションの実装や削除を行うのに必要な変換を記述するupメソッドとdownメソッドの使い方も示されています。これらのメソッドは、add_columnremove_columnのようなマイグレーション固有のメソッドはもちろん、変換で必要なデータを生成する通常のRubyコードも含められます。

データの初期化が必要となる、さらに複雑なマイグレーションの例を以下に示します。

class AddSystemSettings < ActiveRecord::Migration[5.0]
  def up
    create_table :system_settings do |t|
      t.string  :name
      t.string  :label
      t.text    :value
      t.string  :type
      t.integer :position
    end

    SystemSetting.create  name:  'notice',
                          label: 'Use notice?',
                          value: 1
  end

  def down
    drop_table :system_settings
  end
end

このマイグレーションでは最初にsystem_settingsテーブルを追加し、続いて、そのテーブルが依存しているActive Recordを用いる最初の行を作成します。より高度なcreate_tableも用いて、テーブルの完全なスキーマを指定する構文を1つのブロック呼び出しで指定できます。

注: このAPIドキュメントには上述のSystemSetting.createのようにマイグレーション内でモデルを呼び出すコード例がいくつかありますが、そのモデルが将来削除またはリネームされるとdb:migratedb:migrate:resetが失敗する可能性があるので、マイグレーション内でのモデル呼び出しは避けるべきという指摘がBPS Webチームでありました。以下の2つの記事もご覧ください。
参考: How to use models in your migrations (without killing kittens) - The blog of makandra

[Rails 3] 失敗しないmigrationを書こう

利用可能な変換

作成用メソッド

create_join_table (table_1, table_2, options)
最初の2つの引数の辞書式順序(lexical order: いわゆるアルファベット順)を持つjoinテーブルを1つ作成する。詳しくはConnectionAdapters::SchemaStatements#create_join_tableを参照。
create_table(name, options)
nameという名前のテーブルを1つ作成して、そのテーブルオブジェクトをブロックで利用可能にしてカラムを足せるようにする。add_columnと同じフォーマットに従う。上述の例を参照。オプションハッシュは、テーブル作成の定義にappendされる"DEFAULT CHARSET=UTF-8"などのフラグメントに用いられる。
add_column(table_name, column_name, type, options)
table_nameと呼ばれるテーブルにcolumn_nameと呼ばれる新しいカラムを1つ追加する。typeには:string:text:integer:float:decimal:datetime:timestamp:time:date:binary:booleanのいずれかを指定する。デフォルト値は、{ default: 11 }などのoptionsハッシュを渡すことで指定できる。:limit:null(`{ limit: 50, null: false }など)といったその他のオプションについて詳しくは、ActiveRecord::ConnectionAdapters::TableDefinition#columnを参照。
add_foreign_key(from_table, to_table, options)
新しい外部キーを1つ追加する。from_tableはキーのカラムを持つテーブル、to_tableは、参照される主キーを含むテーブルを表す。
add_index(table_name, column_names, options)
指定のカラム名を持つ新しいインデックスを1つ追加する。:name:unique{ name: 'users_name_index', unique: true }など)、:order{ order: { name: :desc } }など)といったオプションもある。
add_reference(:table_name, :reference_name)
reference_name_id(デフォルトでinteger)という新しいカラムを1つ追加する。詳しくはConnectionAdapters::SchemaStatements#add_referenceを参照。
add_timestamps(table_name, options)
タイムスタンプ(created_atupdated_at)カラムをtable_nameに追加する。

変更用メソッド

change_column(table_name, column_name, type, options)
指定のカラムを、add_columnと同じパラメータを用いて別のタイプに変更する。
change_column_default (table_name, column_name, default_or_changes)
table_namedefault_or_changesで定義されたcolumn_nameのデフォルト値を設定する。:from:toを含むハッシュをdefault_or_changesとして渡すと、マイグレーションでこの変更をリバース可能にできる。
change_column_null (table_name, column_name, null, default = nil)
column_nameNOT NULL制約を設定または削除する。nullフラグは、値にNULLを使ってよいかどうかを指定する。詳しくはConnectionAdapters::SchemaStatements#change_column_nullを参照。
change_table(name, options)
nameというテーブルへのカラム変更(ALTER)を行える。このメソッドによって、tableオブジェクトをブロックで利用できるようにしてカラムやインデックスや外部キーを追加または削除できるようにします。
rename_column(table_name, column_name, new_column_name)
カラムをリネームします。タイプや内容は変更しません。
rename_index(table_name, old_name, new_name)
インデックスをリネームします。
rename_table(old_name, new_name)
テーブルをold_nameからnew_nameにリネームします。

削除用メソッド

drop_table(name)
nameというテーブルをDROPします。
drop_join_table(table_1, table_2, options)
引数で指定されたJOINテーブルをDROPします。
remove_column(table_name, column_name, type, options)
table_nameというテーブルからcolumn_nameというカラムを削除します。
remove_columns(table_name, *column_names)
指定のカラムをテーブル定義から削除します。
remove_foreign_key (from_table, to_table = nil, **options)
指定の外部キーをtable_nameというテーブルから削除します。
remove_index(table_name, column: column_names)
インデックスをcolumn_namesで指定して削除します。
remove_index(table_name, name: index_name)
インデックスをindex_nameで指定して削除します。
remove_reference (table_name, ref_name, options)
table_nameにある参照をref_nameで指定して削除します。
remove_timestamps (table_name, options)
タイムスタンプのカラム(created_atupdated_at)をテーブル定義から削除します。

リバースできない変換

変換によっては、破壊的な操作を行うためリバースできないものがあります。そのようなマイグレーションはdownメソッドでActiveRecord::IrreversibleMigration例外が発生します。

Railsの中でマイグレーションを実行する

Railsパッケージには、マイグレーションの作成や適用を支援するさまざまなツールがあります。

新しいマイグレーションの生成には以下のコマンドを使えます。

rails generate migration MyNewMigration

MyNewMigrationはマイグレーションの名前です。この場合ジェネレータによってdb/migrate/ディレクトリの下にタイムスタンプ_my_new_migration.rbというファイルが作成されます。このタイムスタンプは、マイグレーションが生成された日時をUTC形式で表したものです。

テーブルにフィールドを追加するマイグレーションを生成するための、特殊なショートカット構文が利用できます。

rails generate migration add_fieldname_to_tablename fieldname:string

上は以下のような内容のタイムスタンプ_add_fieldname_to_tablename.rbファイルを生成します。

class AddFieldnameToTablename < ActiveRecord::Migration[5.0]
  def change
    add_column :tablenames, :fieldname, :string
  end
end

現在設定されているデータベースへのマイグレーションを実行するには、rails db:migrateコマンドを使います。これによってペンディング中のマイグレーションがすべて実行されてデータベースが更新され、schema_migrationsテーブルが作成されます(後述の「schema_migrationsテーブルについて」を参照)。schema_migrationsテーブルが見当たらない場合は、db:schema:dumpコマンドも実行してdb/schema.rbファイルを更新し、データベース構造と整合させます。

データベースを以前のバージョンのマイグレーションにロールバックするには、rails db:rollback VERSION=Xを使います。このXはダウングレード先のバージョンです。直近のいくつかのマイグレーションにロールバックしたい場合は、STEPオプションを用いることもできます。rails db:rollback STEP=2を実行すると、直近の2つのマイグレーションをロールバックします。

複数のマイグレーションのいずれかでActiveRecord::IrreversibleMigration例外が発生すると、そのステップは失敗します。その場合は手動での作業が必要です。

サポートされるデータベース

現在マイグレーションをサポートしているのは、MySQL、PostgreSQL、SQLite、SQL Server、Oracleです(DB2を除くこれらすべてをサポートします)。

さまざまなマイグレーションの例

マイグレーションによってスキーマが変更されるとは限りません。たとえば、以下のようにデータの修正のみを行うマイグレーションもあります。

class RemoveEmptyTags < ActiveRecord::Migration[5.0]
  def up
    Tag.all.each { |tag| tag.destroy if tag.pages.empty? }
  end

  def down
    # not much we can do to restore deleted data
    raise ActiveRecord::IrreversibleMigration, "Can't recover the deleted tags"
  end
end

次のように、(downではなく)upの場合にカラムを削除するマイグレーションもあります。

class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration[5.0]
  def up
    remove_column :items, :incomplete_items_count
    remove_column :items, :completed_items_count
  end

  def down
    add_column :items, :incomplete_items_count
    add_column :items, :completed_items_count
  end
end

次のように、マイグレーションで直接抽象化を行わず、SQLで何かを実行する必要が生じることもありえます。

class MakeJoinUnique < ActiveRecord::Migration[5.0]
  def up
    execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)"
  end

  def down
    execute "ALTER TABLE `pages_linked_pages` DROP INDEX `page_id_linked_page_id`"
  end
end

モデルのテーブルを変更した後でそのモデルを使う場合の注意

マイグレーションでカラムを1つ追加し、すぐにそれを使いたいことがあります。このような場合、以下の例のようにBase#reset_column_informationを呼んで、新しいカラムの追加後にそのモデルで最新のカラムデータを確実に利用できるようにする必要があります。

class AddPeopleSalary < ActiveRecord::Migration[5.0]
  def up
    add_column :people, :salary, :integer
    Person.reset_column_information
    Person.all.each do |p|
      p.update_attribute :salary, SalaryCalculator.compute(p)
    end
  end
end

コンソール出力を制御する

デフォルトのマイグレーションでは、実行中の操作をその都度コンソールに詳細出力し、各ステップに要した時間を示すベンチマークも出力に含めます。

出力を止めるには、ActiveRecord::Migration.verbose = falseを設定します。

say_with_timeメソッドを使えば、以下のようにメッセージやベンチマークに独自のメッセージを追加することもできます。

def up
  ...
  say_with_time "salaryを更新中..." do
    Person.all.each do |p|
      p.update_attribute :salary, SalaryCalculator.compute(p)
    end
  end
  ...
end

ブロックが完了すると、そのブロックのベンチマークとともに「salaryを更新中...」というフレーズが出力されます。

タイムスタンプ付きのマイグレーション

Railsがデフォルトで生成するマイグレーションファイルは以下のようになります。

20080717013526_your_migration_name.rb

ファイル名の冒頭は、生成時のタイムスタンプ(UTC)を表します。

この形式ではなく、ファイル名の冒頭に数値を使いたい場合は、application.rbで以下を設定することでマイグレーションのタイムスタンプをオフにできます。

config.active_record.timestamped_migrations = false

リバース可能なマイグレーション

リバース可能なマイグレーションとは、マイグレーションをdownで取り消す方法を自動認識するマイグレーションのことです。リバース可能なマイグレーションではupする方法を指定するだけで、downコマンドで実行すべき内容をマイグレーションシステムが認識してくれます。

リバース可能なマイグレーションを定義するには、以下のようにマイグレーションでchangeメソッドを定義します。

class TenderloveMigration < ActiveRecord::Migration[5.0]
  def change
    create_table(:horses) do |t|
      t.column :content, :text
      t.column :remind_at, :datetime
    end
  end
end

上のマイグレーションはupのときにはhorsesテーブルを作成し、downのときにhorsesテーブルをDROPする方法を自動認識します。

コマンドの中には、リバースできないものもあります。そのような場合にupとdownの方法を定義しておきたい場合は、従来と同様にupメソッドとdownメソッドを定義すべきです。

マイグレーションがdownするときにリバースできないコマンドでは、ActiveRecord::IrreversibleMigration例外が発生します。

リバース可能なコマンドリストについてはActiveRecord::Migration::CommandRecorderを参照してください。

トランザクショナルなマイグレーション

DDL(データ定義言語)のトランザクションをサポートするデータベースアダプタでは、すべてのマイグレーションが自動的に1つのトランザクション内にラップされます。クエリによってはトランザクションの中では実行できないものがありますが、そのような場合は次のようにしてトランザクションを自動的にオフにできます。

class ChangeEnum < ActiveRecord::Migration[5.0]
  disable_ddl_transaction!

  def up
    execute "ALTER TYPE model_size ADD VALUE 'new_value'"
  end
end

ただし、self.disable_ddl_transaction!を使うマイグレーションの内部であっても独自のトランザクションをオープンできることを忘れてはいけません。

関連記事

/hachi8833/2021_12_17/77512


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。