Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Railsのマイグレーションを徹底解説!仕組みからロールバック、複数DB対応まで

本記事では、Railsのマイグレーションについて深く掘り下げて解説します。マイグレーションをいくつかの要素に分解しながら、効果的なマイグレーションの書き方を学んでいきましょう。複数のデータベースに対応したマイグレーションの書き方や、失敗したマイグレーションへの対処法、ロールバックの手法についても取り上げます。

記事全体を理解するには、データベースとRailsに関する基礎的な知識が必要です。

マイグレーションの基礎

Railsにおけるマイグレーションとは、アプリケーションのライフサイクルを通じてデータベースを進化させていくための仕組みです。洗練されたDSL(ドメイン固有言語)を提供しており、プレーンなRubyコードを書くだけでデータベースの状態を変更できます。

データベース固有のSQLを書く必要はありません。マイグレーションはデータベースを操作するための抽象化レイヤーを提供し、DSLを背後でデータベース固有のSQLクエリへと変換する面倒な処理もすべて担ってくれます。さらに、生のSQLを実行したい場合にも対応できるよう、そのための手段も用意されています。

Railsデータベースマイグレーションの深層へ

マイグレーションを使えば、テーブルの作成、カラムの追加・削除、カラムへのインデックス付与などを行えます。

すべてのRailsアプリには、マイグレーションファイルを格納するための専用ディレクトリdb/migrateが存在します。

それでは、eventsテーブルを作成するマイグレーションから始めてみましょう。

$ rails g migration CreateEvents category:string

このコマンドを実行すると、db/migrateディレクトリ内に20200405103635_create_events.rbというタイムスタンプ付きのファイルが生成されます。ファイルの中身は以下の通りです。

class CreateEvents < ActiveRecord::Migration[6.0]
  def change
    create_table :events do |t|
      t.string :category
 
      t.timestamps
    end
  end
end

このマイグレーションファイルを分解して見ていきます。

  • Railsが生成するすべてのマイグレーションファイルは、ファイル名にタイムスタンプを持っています。このタイムスタンプは重要で、後述するようにRailsはこれを使ってマイグレーションが実行済みかどうかを判定します。
  • マイグレーションには、ActiveRecord::Migration[6.0]を継承するクラスが含まれています。ここではRails 6を使用しているため、スーパークラスに[6.0]が付いています。Rails 5.2であればActiveRecord::Migration[5.2]となります。スーパークラス名にRailsのバージョンが含まれる理由については、後ほど詳しく説明します。
  • マイグレーションにはchangeメソッドがあり、その中にデータベースを操作するDSLコードが記述されます。この例では、changeメソッドがstring型のcategoryカラムを持つeventsテーブルを作成しています。
  • コードt.timestampsにより、eventsテーブルにcreated_atupdated_atというタイムスタンプカラムが追加されます。

このマイグレーションをrails db:migrateコマンドで実行すると、string型のcategoryカラムと、タイムスタンプカラムであるcreated_atupdated_atを持つeventsテーブルが作成されます。

実際のデータベース上のカラム型は、使用するデータベースに応じてvarcharやtextなどになります。

マイグレーションのタイムスタンプとschema_migrationsテーブルの重要性

rails g migrationコマンドでマイグレーションを生成するたびに、Railsは一意なタイムスタンプを持つマイグレーションファイルを生成します。タイムスタンプの形式はYYYYMMDDHHMMSSです。

マイグレーションが実行されると、Railsはそのタイムスタンプを内部テーブルschema_migrationsに挿入します。このテーブルは、最初のマイグレーションを実行したときにRailsが自動的に作成します。テーブルにはversionカラムのみが存在し、これが主キーでもあります。schema_migrationsテーブルの構造は以下の通りです。

CREATE TABLE IF NOT EXISTS "schema_migrations" ("version" varchar NOT NULL PRIMARY KEY);

eventsテーブルを作成するマイグレーションを実行したので、Railsがこのマイグレーションのタイムスタンプをschema_migrationsテーブルに保存しているか確認してみましょう。

sqlite> select * from schema_migrations;
20200405103635

マイグレーションを再度実行すると、Railsはまずマイグレーションファイルのタイムスタンプに一致するエントリがschema_migrationsテーブルに存在するかを確認し、存在しない場合のみマイグレーションを実行します。これにより、時間の経過とともにデータベースへ段階的に変更を加えることができ、各マイグレーションは確実に一度だけ実行されることが保証されます。

データベーススキーマ

マイグレーションを実行すればするほど、データベーススキーマは進化し続けます。Railsは最新のデータベーススキーマをdb/schema.rbファイルに保存します。このファイルは、アプリケーションのライフサイクルを通じてデータベースに対して実行されたすべてのマイグレーションのRubyによる表現です。

このファイルのおかげで、古いマイグレーションファイルをコードベースに残しておく必要はありません。Railsは、データベースから最新のスキーマをschema.rbにダンプするタスクと、schema.rbからデータベースへスキーマを読み込むタスクを提供しています。したがって、古いマイグレーションは安全に削除できます。また、アプリケーションのセットアップ時に毎回すべてのマイグレーションを順番に実行するよりも、スキーマを読み込む方が高速です。

Railsには、データベーススキーマをSQL形式で保存する方法もあります。両フォーマットの比較については、関連記事をご覧ください。

マイグレーション内のRailsバージョン

生成されるすべてのマイグレーションは、スーパークラス名の一部としてRailsのバージョンを含みます。Rails 6アプリで生成されたマイグレーションのスーパークラスはActiveRecord::Migration[6.0]であり、Rails 5.2アプリで生成されたものはActiveRecord::Migration[5.2]です。Rails 4.2以前の古いアプリでは、スーパークラスにバージョン表記がなく、単にActiveRecord::Migrationとなっていることに気づくでしょう。

このバージョン表記はRails 5で導入されました。これにより、古いバージョンのRailsで生成されたマイグレーションを壊すことなく、マイグレーションAPIを時間とともに進化させることができます。

具体的に見てみましょう。Rails 4.2アプリで同じeventsテーブルを作成するマイグレーションを書くと、以下のようになります。

class CreateEvents < ActiveRecord::Migration
  def change
    create_table :events do |t|
      t.string :category
 
      t.timestamps null: false
    end
  end
end

Rails 6のマイグレーションによって生成されたeventsテーブルのスキーマを見ると、タイムスタンプカラムにNOT NULL制約が付与されていることがわかります。

sqlite> .schema events
CREATE TABLE IF NOT EXISTS "events" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "category" varchar, "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL);

これは、Rails 5以降では、マイグレーションAPIがタイムスタンプカラムに自動的にNOT NULL制約を追加するためです。マイグレーションファイルに明示的に記述する必要はありません。

スーパークラス名に含まれるRailsのバージョンにより、マイグレーションは生成時のRailsバージョンのマイグレーションAPIを使用するようになります。これにより、古いマイグレーションとの後方互換性を維持しながら、マイグレーションAPIを進化させることが可能になっています。

データベーススキーマの変更

changeメソッドはマイグレーションにおける主要なメソッドです。マイグレーションが実行されるとchangeメソッドが呼び出され、その中のコードが実行されます。

create_tableに加えて、Railsはもうひとつの強力なメソッドchange_tableを提供しています。その名の通り、既存テーブルのスキーマを変更するために使います。

def change
  change_table :events do |t|
    t.remove :category
    t.string :event_type
    t.boolean :active, default: false
  end
end

このマイグレーションは、eventsテーブルからcategoryカラムを削除し、新しいstring型カラムevent_typeと、デフォルト値がfalseのboolean型カラムactiveを追加します。

マイグレーション内で使えるその他のヘルパーメソッドも多数用意されています。例えば以下のようなものです。

  • change_column
  • add_index
  • remove_index
  • rename_table

その他、changeで使用できるすべてのメソッドについては公式ドキュメントを参照してください。

タイムスタンプ

先ほど見たように、Railsはt.timestampsをマイグレーションに追加し、それによってeventsテーブルにcreated_atupdated_atカラムが作成されました。これらの特殊なカラムは、Railsがレコードの作成日時と更新日時を追跡するために使われます。

Railsはレコード作成時にこれらのカラムへ値を設定し、レコード更新時には必ず値を更新します。これらのカラムがあることで、データベースレコードのライフサイクルを追跡できます。

なお、Railsのupdate_allメソッドを実行した場合は、updated_atカラムは更新されません。

失敗への対処

マイグレーションは万全ではありません。失敗することもあります。原因は構文ミスかもしれないし、無効なデータベースクエリかもしれません。原因が何であれ、データベースが不整合な状態にならないよう、失敗に対処して復旧する必要があります。

Railsはこの問題を、各マイグレーションをトランザクション内で実行することで解決しています。マイグレーションが失敗するとトランザクションはロールバックされるため、データベースが不整合な状態に陥ることはありません。

これは、データベーススキーマの更新に関するトランザクション(DDLトランザクション)をサポートするデータベースでのみ機能します。MySQLとPostgreSQLはどちらもDDLトランザクションをサポートしています。

場合によっては、特定のマイグレーションをトランザクション内で実行したくないこともあります。簡単な例として、PostgreSQLでのコンカレントインデックスの追加が挙げられます。このようなマイグレーションはDDLトランザクション内で実行できません。PostgreSQLはテーブルにロックをかけずにインデックスを追加しようとするため、稼働中の本番データベースをダウンさせることなくインデックスを追加できるのです。Railsでは、disable_ddl_transactions!を使うことで、マイグレーションをトランザクション外で実行できます。

def change
  disable_ddl_transactions!
 
  add_index :events, :user_id, algorithm: :concurrently

こうすると、マイグレーションはトランザクション内で実行されません。この種のマイグレーションが失敗した場合は、自分で復旧する必要があります。このケースでは、REINDEXを実行するか、一度インデックスを削除して再追加を試みることになります。

可逆マイグレーション

Railsでは、次のコマンドでデータベースへの変更をロールバックできます。

rails db:rollback

このコマンドは、最後に実行されたマイグレーションを元に戻します。マイグレーションがevent_typeカラムを追加していたなら、ロールバックするとそのカラムが削除されます。インデックスを追加していたなら、ロールバックでそのインデックスが削除されます。

直前のマイグレーションをロールバックしてから再実行するコマンドもあります。それがrails db:redoです。

Railsはほとんどのマイグレーションの逆操作を賢く判断できます。しかし、upメソッドとdownメソッドをchangeメソッドの代わりに使うことで、マイグレーションの戻し方をRailsにヒントとして与えることもできます。upメソッドはマイグレーション実行時に使われ、downメソッドはロールバック時に使われます。

def up
  change_table :events do |t|
    t.change :price, :string
  end
end
 
def down
  change_table :events do |t|
    t.change :price, :integer
  end
end

この例では、eventsテーブルのpriceカラムをintegerからstringに変更しています。そしてdownメソッドで、ロールバック時の動作を指定しています。

同じマイグレーションはchangeメソッドでも書けます。

def change
  reversible do |direction|
    change_table :events do |t|
      direction.up { t.change :price, :string }
      direction.down { t.change :price, :integer }
    end
  end
end

さらにRailsは、revertメソッドを使って以前のマイグレーションを完全に取り消すこともできます。

def change
  revert CreateEvents
 
  create_table :events do
   ...
  end
end

revertメソッドはブロックを受け取ることができ、マイグレーションの一部だけを取り消すことも可能です。

def change
  revert do
    reversible do |direction|
      change_table :events do |t|
        direction.up { t.remove :event_type }
        direction.down { t.string :event_type }
      end
    end
  end
end

生のSQLを実行する

マイグレーション内で複雑なSQLを実行したい場合もあるでしょう。そのようなときは、通常のマイグレーションDSLを忘れて、代わりに生のSQLを以下のように実行できます。

def change
  execute <<-SQL
    ....
  SQL
end

複数データベースとマイグレーション

Rails 6では、1つのRailsアプリケーション内で複数のデータベースを使用する機能が追加されました。複数のデータベースを使いたい場合は、database.ymlファイルで設定します。

development:
  primary:
    <<: *default
    database: db/development.sqlite3
  analytics:
    adapter: sqlite3
    database: db/analytics_dev.sqlite3

この設定により、Railsはprimaryanalyticsの2つのデータベースを使うことを認識します。前述の通り、マイグレーションはデフォルトでdb/migrateディレクトリに格納されます。しかし、この場合、両方のデータベースのマイグレーションを1つのディレクトリに入れることはできません。analyticsデータベースのマイグレーションをprimaryデータベースに対して実行したり、その逆になったりしては困りますよね。複数のデータベースを使う場合は、2つ目以降のデータベースのマイグレーション保存先パスを指定する必要があります。これはdatabase.ymlmigrations_pathsを指定することで実現できます。

development:
  primary:
    <<: *default
    database: db/development.sqlite3
  analytics:
    adapter: sqlite3
    database: db/analytics_dev.sqlite3
    migrations_paths: db/analytics_migrate

これで、analyticsデータベース向けのマイグレーションを次のように作成できます。

rails generate migration AddExperiments rule:string active:boolean --db=analytics

これにより、マイグレーションはdb/analytics_migrate内に作成され、次のコマンドで実行できます。

rails db:migrate --db=analytics

オプションを付けずに単にrails db:migrateを実行すると、すべてのデータベースのマイグレーションが実行されます。

analyticsデータベースには独自のschema_migrationsテーブルがあり、どのマイグレーションが実行済みでどれが未実行かを管理します。

デプロイ時にマイグレーションを実行する

マイグレーションはデータベースの状態を変えるものであり、コードがそれらの変更に依存している可能性があります。そのため、新しいコードを適用する前に必ずマイグレーションを実行しておくことが極めて重要です。

Herokuベースのデプロイでは、Procfilereleaseフェーズでマイグレーションを実行できます。

# Procfile
web: bin/puma -C config/puma.rb
release: bundle exec rake db:migrate

これにより、app dynoが再起動される前にマイグレーションが実行されることが保証されます。

Capistranoベースのデプロイでは、サーバーを再起動する前にマイグレーションを実行するようにします。

Dockerベースのデプロイでは、アプリを再起動する前にマイグレーションを実行するサイドカーコンテナを実行できます。これは非常に重要です。そうしないと、新しいコンテナがデータベース変更を適用する前に新コードを使い始め、不整合な状態に陥る恐れがあるからです。

まとめ

本記事では、Railsでデータベースマイグレーションを書く際のさまざまな側面を見てきました。マイグレーションを構成する要素や、失敗への対処法、必要に応じたロールバックの方法についても解説しました。また、Rails 6では複数のデータベースを利用でき、それぞれのマイグレーションを個別に追加する必要があることにも触れました。最後に、新しいコードがデータベースの変更を使い始める前に適切に適用されるよう、デプロイ時にマイグレーションを実行する方法を簡単に紹介しました。

P.S. Ruby Magicの記事を公開と同時にお読みになりたい方は、Ruby Magicニュースレターを購読して、記事を見逃さないようにしましょう!

  1. React on Rails:シンプルなアプリの構築

    アプリケーションのフロントエンド側全体を構築する企業は、多くの場合、バックエンドを構築するためにRailsなどの同じフレームワークを選択します。長年にわたり、これは最良かつ最も信頼できるオプションでした。 今日、絶えず進化するフロントエンドユニバースにある多数のライブラリとフレームワークにより、開発者はバックエンドとフロントエンドの両方に異なるプラットフォームを選択し、それらを簡単に統合できます。 Reactはフロントエンドのパンゲアの巨人になりました。 Ruby on Railsを使用している場合は、デフォルトのRailsページをReactコード(またはその他のフロントフレームワーク)に

  2. Rails5でのAngularの使用

    あなたは前にその話を聞いたことがあります。分散型で完全に機能するバックエンドAPIと、通常のツールセットで作成されたフロントエンドで実行されているアプリケーションがすでにあります。 次に、Angularに移動します。または、AngularをRailsプロジェクトと統合する方法を探しているだけかもしれません。これは、この方法を好むためです。私たちはあなたを責めません。 このようなアプローチを使用すると、両方の世界を活用して、たとえばRailsとAngularのどちらの機能を使用してフォーマットするかを決定できます。 構築するもの 心配する必要はありません。このチュートリアルは、この目的のた