Active Record マイグレーション

マイグレーション(migration)はActive Recordの機能の1つであり、データベーススキーマが長期にわたって進化を安定して繰り返せるようにするための仕組みです。マイグレーション機能のおかげで、スキーマ変更を生SQLで記述せずに、Rubyで作成されたマイグレーション用のDSL(ドメイン固有言語)を用いてテーブルの変更を簡単に記述できます。

このガイドの内容:

  • マイグレーション作成でどんなジェネレータを利用できるか
  • Active Recordでどんなデータベース操作用メソッドが提供されているか
  • 既存のマイグレーションの変更方法や、スキーマの更新方法
  • マイグレーションとスキーマファイルschema.rbの関係
  • 参照整合性を維持する方法

目次

  1. マイグレーションの概要
  2. マイグレーションファイルを生成する
  3. マイグレーションを更新する
  4. マイグレーションを実行する
  5. 既存のマイグレーションを変更する
  6. スキーマダンプの意義
  7. Active Recordと参照整合性
  8. マイグレーションとseedデータ
  9. 古いマイグレーション
  10. その他

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

マイグレーションは、再現可能な方法でデータベーススキーマを継続的に進化させる便利な方法です。

マイグレーションではRubyのDSLを利用しているので、SQLを手動で記述しなくても済み、スキーマやスキーマの変更がデータベースに依存しないようにできます。ここで説明した概念のいくつかについて詳しくは、Active Record基礎ガイドおよびActive Recordの関連付けガイドを読むことをオススメします。

個別のマイグレーションは、データベースの新しい「バージョン」とみなせます。スキーマは空の状態から始まり、マイグレーションによる変更が加わるたびにテーブルやカラムやインデックスがスキーマに追加・削除されます。Active Recordはマイグレーションの時系列に沿ってスキーマを更新する方法を知っているので、履歴のどの時点からでも最新バージョンのスキーマに更新できます。Railsがタイムライン内のどのマイグレーションを実行するかを認識する方法について詳しくは、後述するマイグレーションのバージョン管理を参照してください。

Active Recordはdb/schema.rbファイルを更新し、データベースの最新の構造と一致するようにします。マイグレーションの例を以下に示します。

# db/migrate/20240502100843_create_products.rb
class CreateProducts < ActiveRecord::Migration[7.2]
  def change
    create_table :products do |t|
      t.string :name
      t.text :description

      t.timestamps
    end
  end
end

上のマイグレーションを実行するとproductsという名前のテーブルが追加されます。この中にはnameというstringカラムと、descriptionというtextカラムが含まれています。主キーはidという名前で暗黙に追加されます(idはActive Recordモデルにおけるデフォルトの主キーです)。timestampsマクロは、created_atupdated_atという2つのカラムを追加します。これらの特殊なカラムが存在する場合、Active Recordによって自動的に管理されます。

# db/schema.rb
ActiveRecord::Schema[7.2].define(version: 2024_05_02_100843) do
  # 以下はこのデータベースをサポートするうえで有効にしなければならない拡張機能
  enable_extension "plpgsql"

  create_table "products", force: :cascade do |t|
    t.string "name"
    t.text "description"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end
end

今後、時間軸に沿って行いたい変更を定義します。このマイグレーションを実行する前は、データベースにテーブルは存在しません。マイグレーションの実行後はテーブルが存在するようになります。 Active Recordは、このマイグレーションを元に戻す方法も認識しています。このマイグレーションをロールバックすると、テーブルが削除されます。マイグレーションのロールバックの詳細については、ロールバックセクションを参照してください。

時間軸に沿って行いたい変更を定義した後は、マイグレーションをロールバック可能にすることを考慮しておくことが重要です。Active Recordは、マイグレーションの進行を管理することでテーブルを確実に作成できますが、可逆性の概念が重要になります。マイグレーションが可逆的に作られていれば、マイグレーションを適用してテーブルを作成できるだけでなく、スムーズなロールバック機能も有効になります。 上記のマイグレーションを元に戻す場合、Active Recordはテーブルの削除をインテリジェントに処理し、マイグレーション作業全体でデータベースの一貫性を維持します。詳しくは、以前のマイグレーションに戻すセクションを参照してください。

2 マイグレーションファイルを生成する

2.1 単独のマイグレーションを作成する

マイグレーションは、マイグレーションクラスごとに1個ずつdb/migrateディレクトリにファイルとして保存されます。

ファイル名は YYYYMMDDHHMMSS_create_products.rbという形式で、マイグレーションを識別するUTCタイムスタンプ、アンダースコア、マイグレーション名で構成されます。CamelCaseで記述するクラス名は、マイグレーションファイル名の後半部分と一致しなければなりません。

たとえば、20240502100843_create_products.rbというマイグレーションファイルではCreateProductsクラスを定義し、20240502101659_add_details_to_products.rbというマイグレーションファイルではAddDetailsToProductsクラスを定義する必要があります。Railsはこのタイムスタンプを手がかりにして、どのマイグレーションをどの順序で実行するかを決定します。そのため、別のアプリケーションからマイグレーションをコピーする場合や自分でファイルを生成する場合は、順序に注意してください。タイムスタンプの利用方法について詳しくは、マイグレーションのバージョン管理セクションを参照してください。

Active Recordはマイグレーションファイルを生成するときに、マイグレーションのファイル名の冒頭に現在のタイムスタンプを自動的に追加します。たとえば、以下のコマンドを実行すると、アンダースコア形式のマイグレーション名の前にタイムスタンプが追加されたファイル名を持つ、空のマイグレーションファイルが作成されます。

$ bin/rails generate migration AddPartNumberToProducts
# db/migrate/20240502101659_add_part_number_to_products.rb
class AddPartNumberToProducts < ActiveRecord::Migration[7.2]
  def change
  end
end

ジェネレータは、単にファイル名の冒頭にタイムスタンプを追加するだけではありません。命名規約や追加の(オプション)引数に基づいて、マイグレーションに肉付けすることもできます。

次のセクションでは、規約と追加の引数に基づいてマイグレーションを作成するさまざまな方法について解説します。

2.2 新しいテーブルを作成する

データベースに新しいテーブルを作成する場合は、「CreateXXX」という形式のマイグレーション名を指定して、その後にカラム名と型のリストを指定するようにしてください。こうすることで、指定したカラムでテーブルをセットアップするマイグレーションファイルが生成されます。

$ bin/rails generate migration CreateProducts name:string part_number:string

上を実行すると以下のマイグレーションファイルが生成されます。

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

      t.timestamps
    end
  end
end

ここまでに生成したマイグレーションの内容は、必要に応じてこれを元に作業するための単なる出発点でしかありません。db/migrate/YYYYMMDDHHMMSS_add_details_to_products.rbファイルを編集して、項目の追加や削除を行えます。

2.3 カラムを追加する

データベース内の既存のテーブルに新しいカラムを追加する場合は、「AddColumnToTable」という形式のマイグレーション名を指定して、その後にカラム名と型のリストを指定するようにしてください。こうすることで、適切なadd_columnステートメントを含むマイグレーションファイルが生成されます。

$ bin/rails generate migration AddPartNumberToProducts part_number:string

上を実行すると以下のマイグレーションファイルが生成されます。

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

新しいカラムにインデックスも追加したい場合は以下のようにコマンドを実行します。

$ bin/rails generate migration AddPartNumberToProducts part_number:string:index

上を実行すると以下のように適切なadd_columnadd_indexステートメントが生成されます。

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

自動生成できるカラムは1個だけではありません。たとえば以下のように複数のカラムも指定できます。

$ bin/rails generate migration AddDetailsToProducts part_number:string price:decimal

上を実行すると、productsテーブルに2個のカラムを追加するスキーママイグレーションを生成します。

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

2.4 カラムを削除する

同様に、「RemoveColumnFromTable」という形式のマイグレーション名を指定し、その後にカラム名と型のリストを与えることで、適切なremove_columnステートメントを含むマイグレーションファイルが作成されます。

$ bin/rails generate migration RemovePartNumberFromProducts part_number:string

上を実行すると、適切なremove_columnステートメントが生成されます。

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

2.5 関連付けを作成する

Active Recordの関連付け(association)は、アプリケーション内のさまざまなモデル間のリレーションシップを定義するのに使われ、モデル同士がリレーションシップを通じてやり取りできるようにすることで、互いに関連するデータを操作しやすくします。関連付けについて詳しくは、関連付けのガイドを参照してください。

関連付けの一般的なユースケースの1つは、テーブル間の外部キー参照を作成することです。Railsのマイグレーションジェネレーターは、この作業を軽減するために、referencesなどのカラム型を渡せるようになっています。references型は、カラム、インデックス、外部キー、またはポリモーフィック関連付けカラムを作成するためのショートハンドです。

$ bin/rails generate migration AddUserRefToProducts user:references

たとえば上を実行すると、以下のadd_reference呼び出しが生成されます。

class AddUserRefToProducts < ActiveRecord::Migration[7.2]
  def change
    add_reference :products, :user, null: false, foreign_key: true
  end
end

このマイグレーションを実行すると、productsテーブルにuser_idが作成されます。ここでuser_idは、usersテーブルのidカラムへの参照です。また、user_idカラムのインデックスも作成されます。 マイグレーション実行後のスキーマは次のようになります。

  create_table "products", force: :cascade do |t|
    t.bigint "user_id", null: false
    t.index ["user_id"], name: "index_products_on_user_id"
  end

belongs_toreferencesのエイリアスなので、上述のマイグレーションコマンドは次のようにも記述できます。

$ bin/rails generate migration AddUserRefToProducts user:belongs_to

このマイグレーションコマンドで生成されるマイグレーションファイルやスキーマファイルは、上述のものと同じです。

名前の一部にJoinTableが含まれているとjoinテーブルを生成するジェネレータもあります。

$ bin/rails generate migration CreateJoinTableUserProduct user product

上によって以下のマイグレーションが生成されます。

class CreateJoinTableUserProduct < ActiveRecord::Migration[7.2]
  def change
    create_join_table :users, :products do |t|
      # t.index [:user_id, :product_id]
      # t.index [:product_id, :user_id]
    end
  end
end

2.6 その他のマイグレーション作成用ジェネレータ

migrationジェネレータの他に、modelジェネレータ、resourceジェネレータ、scaffoldジェネレーターは、それぞれ新しいモデルを追加するのに適したマイグレーションを作成します。これらのマイグレーションには、関連するテーブルを作成するための手順がすでに含まれています。必要なカラムをRailsに指示すると、これらのカラムを追加するためのステートメントも作成されます。 たとえば、以下のコマンドを実行するとします。

$ bin/rails generate model Product name:string description:text

これにより、以下のようなマイグレーションが生成されます。

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

      t.timestamps
    end
  end
end

カラム名と型のペアは、好きなだけマイグレーションコマンドに追加できます。

2.7 修飾子を渡す

コマンドでマイグレーションを生成するときに、よく使われる型修飾子をコマンドラインで直接指定できます。これらの修飾子は波かっこ{}で囲んでフィールド型の後ろに置きます。これにより、後でマイグレーションファイルを手動で編集せずにデータベースカラムをカスタマイズできます。

たとえば以下を実行したとします。

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

これによって以下のようなマイグレーションが生成されます。

class AddDetailsToProducts < ActiveRecord::Migration[7.2]
  def change
    add_column :products, :price, :decimal, precision: 5, scale: 2
    add_reference :products, :supplier, polymorphic: true
  end
end

詳しくはジェネレータのヘルプ(bin/rails generate --help)を参照してください。または、bin/rails generate model --helpbin/rails generate migration --helpを実行して特定のジェネレーターのヘルプを表示することも可能です。

3 マイグレーションを更新する

前述のマイグレーションファイルを生成するセクションのいずれかのジェネレーターを使ってマイグレーションファイルを作成したら、db/migrateフォルダ内に生成されたマイグレーションファイルを更新して、データベーススキーマに加えたい変更を追加で定義できます。

3.1 テーブルを作成する

create_tableは最も基本的なマイグレーションメソッドですが、手書きするよりも、モデルジェネレータやリソースジェネレータやscaffoldジェネレータで生成することがほとんどです。典型的な利用法は以下のとおりです。

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

上のメソッドは、productsテーブルを作成し、nameという名前のカラムをその中に作成します。

3.1.1 関連付け

関連付けを持つモデルのテーブルを作成する場合は、以下のように:references型を指定することで適切なカラム型を作成できます。

create_table :products do |t|
  t.references :category
end

上のマイグレーションによってcategory_idカラムが作成されます。以下のように、belongs_toの代わりにreferencesをエイリアスとして使うことも可能です。

create_table :products do |t|
  t.belongs_to :category
end

:polymorphicオプションを使って、以下のようなカラム型とインデックス作成を指定することも可能です。

create_table :taggings do |t|
  t.references :taggable, polymorphic: true
end

上のマイグレーションによって、taggable_idカラムとtaggable_typeカラムと適切なインデックスが作成されます。

3.1.2 主キー

create_tableメソッドは、デフォルトでidという名前の主キーを暗黙で作成します。:primary_keyオプションを使うと、以下のようにカラム名を変更できます。

class CreateUsers < ActiveRecord::Migration[7.2]
  def change
    create_table :users, primary_key: "user_id" do |t|
      t.string :username
      t.string :email
      t.timestamps
    end
  end
end

上のマイグレーションで以下のスキーマが生成されます。

create_table "users", primary_key: "user_id", force: :cascade do |t|
  t.string "username"
  t.string "email"
  t.datetime "created_at", precision: 6, null: false
  t.datetime "updated_at", precision: 6, null: false
end

複合主キーの場合は、以下のように:primary_keyに配列も渡せます。複合主キーについて詳しくは複合主キーガイドを参照してください。

class CreateUsers < ActiveRecord::Migration[7.2]
  def change
    create_table :users, primary_key: [:id, :name] do |t|
      t.string :name
      t.string :email
      t.timestamps
    end
  end
end

主キーを使いたくない場合は、以下のようにid: falseオプションを指定することも可能です。

class CreateUsers < ActiveRecord::Migration[7.2]
  def change
    create_table :users, id: false do |t|
      t.string :username
      t.string :email
      t.timestamps
    end
  end
end
3.1.3 データベースオプション

特定のデータベースに依存するオプションが必要な場合は、以下のように:optionsオプションに続けてSQLフラグメントを記述します。

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

上のマイグレーションでは、テーブルを生成するSQLステートメントにENGINE=BLACKHOLEを追加しています。

以下のように、index: trueを渡すか、:indexオプションにオプションハッシュを渡すと、create_tableブロックで作成されるカラムにインデックスを追加できます。

create_table :users do |t|
  t.string :name, index: true
  t.string :email, index: { unique: true, name: 'unique_emails' }
end
3.1.4 コメント

:commentオプションを使うと、テーブルを説明するコメントを書いてデータベース自身に保存することも可能です。保存した説明文はMySQL WorkbenchやPgAdmin IIIなどのデータベース管理ツールで表示できます。説明文を追加しておくことでチームメンバーがデータモデルを理解しやすくなり、大規模なデータベースを持つアプリケーションでドキュメントを生成するのに役立ちます。 現時点では、MySQLとPostgreSQLアダプタのみがコメント機能をサポートしています。

class AddDetailsToProducts < ActiveRecord::Migration[7.2]
  def change
    add_column :products, :price, :decimal, precision: 8, scale: 2, comment: "製品価格(ドル)"
    add_column :products, :stock_quantity, :integer, comment: "現在の製品在庫数"
  end
end

3.2 joinテーブルを作成する

マイグレーションのcreate_join_tableメソッドは、has_and_belongs_to_many(HABTM)というjoinテーブルを作成します。典型的な利用法は以下のとおりです。

create_join_table :products, :categories

上によってcategories_productsテーブルが作成され、その中にcategory_idカラムとproduct_idカラムが生成されます。

これらのカラムはデフォルトで:nullオプションがfalseに設定されます。これは、このテーブルにレコードを保存するためには必ず何らかの値を指定しなければならないことを意味します。これは、以下のように:column_optionsオプションを指定することで上書きできます。

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

デフォルトでは、create_join_tableに渡された引数の最初の2つをレキシカルな順序(出現順)につなげたものがjoinテーブル名になります。この場合、テーブル名はcategories_productsになります。

モデル名同士の優先順位は、String<=>演算子で算出されます。一般に、文字列同士の長さが異なり、かつ、最も短い文字列の長さまで比較したときの文字列が一致する場合(短い文字列が長い文字列の出だしと完全に一致する場合)、一般に長い文字列の方が短い文字列よりも辞書的な優先順位が低い(辞書的な並び順で後に出現する)と見なされます。たとえば、"papers"と"paper"であれば、"paper", "papers"の順になります。

ただし、エンコーディングや文字によっては、短い文字列が長い文字列の出だしと完全に一致する場合であっても、 長い文字列の方が短い文字列よりも辞書的な優先順位が高い(辞書的な並び順で前に出現する)と見なされることがあります。たとえば、"paper_boxes"テーブルと"papers"テーブルを結合すると、上記の一般的な振る舞いから"paper_boxes"という名前の文字列の方が長いので、生成される結合テーブル名は"papers_paper_boxes"になるように思うかもしれませんが、一般的なエンコーディングでは、アンダースコア'_'は辞書的な並び順で's'よりも前に出現するため、実際に生成される結合テーブル名はその逆の"paper_boxes_papers"になります。

独自のテーブル名を使いたい場合は、:table_nameで指定します。

create_join_table :products, :categories, table_name: :categorization

上のようにすることでcategorizationという名前のjoinテーブルが作成されます。

create_join_tableにはブロックも渡せます。ブロックはインデックスの追加(インデックスはデフォルトでは作成されません)やカラムの追加に使われます。

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

3.3 テーブルを変更する

既存のテーブルを変更するchange_tableは、create_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

上のマイグレーションではdescriptionnameカラムが削除され、stringカラムであるpart_numberが作成されてインデックスが追加されます。最後にupccodeカラムをupc_codeにリネームしています。

3.4 カラムを変更する

Railsのマイグレーションでは、上述したremove_columnadd_columnと同様に、change_columnメソッドも利用できます。

change_column :products, :part_number, :text

上は、productsテーブル上のpart_numberカラムの型を:textフィールドに変更しています。

change_columnコマンドは逆進できない点にご注意ください。マイグレーションを安全に元に戻せるようにするには、reversibleなマイグレーションを独自に提供する必要があります。詳しくは、reversibleを使うを参照してください。

change_columnの他に、カラムのnull制約を変更するchange_column_nullメソッドや、カラムのデフォルト値を指定するchange_column_defaultメソッドも利用できます。

change_column_default :products, :approved, from: true, to: false

上のマイグレーションは、:approvedフィールドのデフォルト値をtrueからfalseに変更します。これらの変更は、どちらも今後のトランザクションにのみ適用され、既存のレコードには適用されない点にご注意ください。

null制約を変更するには、change_column_nullを使います。

change_column_null :products, :name, false

上のマイグレーションは、productsの:nameフィールドを NOT NULLカラムに設定します。この変更は既存のレコードにも適用されるため、既存のすべてのレコードの:nameNOT NULLになっていることを確認する必要があります。

NULL制約をtrueに設定すると、そのカラムはNULL値を許容するようになります。falseに設定するとNOT NULL制約が適用され、レコードをデータベースに永続化するためには(NULL以外の)何らかの値を渡す必要があります。

上のchange_column_defaultマイグレーションはchange_column_default :products, :approved, falseと書くことも可能ですが、先ほどの例と異なり、マイグレーションは逆進できなくなります。

3.5 カラム修飾子

カラムの作成時や変更時に、カラムの修飾子を適用できます。

  • comment: カラムにコメントを追加します。
  • collation: stringカラムやtextカラムのコレーション(照合順序)を指定します。
  • default: カラムでのデフォルト値の設定を許可します。dateなどの動的な値を使う場合は、デフォルト値は初回(すなわちマイグレーションが実行された日付)しか計算されないことにご注意ください。デフォルト値をNULLにする場合はnilを指定してください。
  • limit: stringフィールドについては最大文字数を、text/binary/integerについては最大バイト数を設定します。
  • null: カラムでNULL値を許可または禁止します。
  • precision: decimal/numeric/datetime/timeフィールドの精度(precision)を定義します。
  • scale: decimal/numericフィールドのスケールを指定します。スケールは小数点以下の桁数で表されます。

add_columnchange_columnにはインデックス追加用のオプションはありません。add_indexで別途インデックスを追加する必要があります。

アダプタによっては他にも利用できるオプションがあります。詳しくは各アダプタ固有のAPIドキュメントを参照してください。

nulldefaultは、コマンドラインでマイグレーションを生成するときには指定できません。

3.6 参照

add_referenceメソッドを使うと、1個以上の関連付け同士のつながりとして振る舞う適切な名前のカラムを作成できます。

add_reference :users, :role

上のマイグレーションは、usersテーブルにrole_idという外部キーカラムを作成します。role_idは、rolesテーブルのidカラムへの参照です。さらに、role_idカラムのインデックスも作成されます(index: falseオプションで明示的に無効にしない限り)。

詳しくはActive Record の関連付けガイドも参照してください。

add_belongs_toメソッドはadd_referenceのエイリアスです。

add_belongs_to :taggings, :taggable, polymorphic: true

polymorphic:オプションは、taggingsテーブルにtaggable_typeカラムおよびtaggable_idカラムというポリモーフィック関連付け用のカラムを2つ作成します。

詳しくはポリモーフィック関連付けも参照してください。

foreign_keyオプションを指定すると外部キーを作成できます。

add_reference :users, :role, foreign_key: true

add_referenceオプションについて詳しくはAPIドキュメントを参照してください。

remove_referenceで以下のように参照を削除できます。

remove_reference :products, :user, foreign_key: true, index: false

3.7 外部キー

参照整合性の保証に対して外部キー制約を追加することも可能です。これは必須ではありません。

add_foreign_key :articles, :authors

上のadd_foreign_key呼び出しは、articlesテーブルに新たな制約を追加します。この制約によって、idカラムがarticles.author_idと一致する行がauthorsテーブル内に存在することが保証され、articlesテーブルにリストされているすべてのレビュー担当者が、authorsテーブルにリストされている有効な著者であることが保証されます。

マイグレーションでreferencesを使う場合、テーブルに新しいカラムを作成して、そのカラムにforeign_key: trueで外部キーを追加するオプションもあります。ただし、既存のカラムに外部キーを追加する場合は、add_foreign_keyを使えます。

参照される主キーを持つテーブルから、外部キーを追加するテーブルのカラム名を導出できない場合は、:columnオプションでカラム名を指定できます。また、参照される主キーが:idでない場合は、:primary_keyオプションを利用できます。

たとえば、authors.emailを参照するarticles.reviewerに外部キーを追加するには以下のようにします。

add_foreign_key :articles, :authors, column: :reviewer, primary_key: :email

上はarticlesテーブルに制約を追加します。この制約は、emailカラムがarticles.reviewerフィールドと一致する行が、authorsテーブルに存在することを保証します。

add_foreign_keyでは、nameon_deleteif_not_existsvalidatedeferrableなどのオプションもサポートされています。

外部キーの削除も以下のようにremove_foreign_keyで行えます。

# 削除するカラム名の決定をActive Recordに任せる場合
remove_foreign_key :accounts, :branches

# カラムを指定して外部キーを削除する場合
remove_foreign_key :accounts, column: :owner_id

Active Recordでは単一カラムの外部キーのみがサポートされています。複合外部キーを使う場合はexecutestructure.sqlが必要です。詳しくはスキーマダンプの意義を参照してください。

3.8 複合主キー

1個のカラム値だけではテーブルの各行を一意に識別するのに不十分でも、2つ以上のカラムを組み合わせれば一意に識別できる場合があります。この状況は、主キーとして単一のidカラムを持たない既存のレガシーデータベースのスキーマを利用する場合や、シャーディングやマルチテナンシー向けにスキーマを変更する場合に起こる可能性があります。

create_tableで以下のように:primary_keyオプションと配列の値を渡すことで、複合主キー(composite primary key)を持つテーブルを作成できます。

class CreateProducts < ActiveRecord::Migration[7.2]
  def change
    create_table :products, primary_key: [:customer_id, :product_sku] do |t|
      t.integer :customer_id
      t.string :product_sku
      t.text :description
    end
  end
end

複合主キーを持つテーブルでは、多くのメソッドで整数のIDではなく配列値を渡す必要があります。詳しくは、複合主キーガイドも参照してください。

3.9 生SQLを実行する

Active Recordが提供するヘルパーの機能だけでは不十分な場合、executeメソッドで任意のSQLを実行できます。

class UpdateProductPrices < ActiveRecord::Migration[7.2]
  def up
    execute "UPDATE products SET price = 'free'"
  end

  def down
    execute "UPDATE products SET price = 'original_price' WHERE price = 'free';"
  end
end

上の例では、productsテーブルのpriceカラムを全レコードについて'free'に更新しています。

データをマイグレーションでみだりに直接変更しないよう注意が必要です。データを(rakeタスクやRailsランナーなどではなく)マイグレーションで変更することが本当にユースケースに最適な方法であるかどうかを慎重に検討し、複雑さやメンテナンスのオーバーヘッドが増加しないかどうか、データの整合性やデータベースの移植性に対するリスクが増加しないか、といった潜在的な欠点に十分注意してください。詳しくは、データのマイグレーションセクションを参照してください。

個別のメソッドについて詳しくは、APIドキュメントを確認してください。

特に、ActiveRecord::ConnectionAdapters::SchemaStatementsは、changeupdownメソッドで利用可能なメソッドを提供します。

create_tableで生成されるオブジェクトで利用可能なメソッドについては、ActiveRecord::ConnectionAdapters::TableDefinitionを参照してください。

change_tableで生成されるオブジェクトで利用可能なメソッドについては、ActiveRecord::ConnectionAdapters::Tableを参照してください。

3.10 changeメソッドを使う

changeメソッドは、マイグレーションを自作する場合に最もよく使われます。このメソッドを使えば、多くの場合にActive Recordがマイグレーションを逆進させる(以前のマイグレーションにロールバックする)方法を自動的に認識します。以下はchangeでサポートされているマイグレーション定義の一部です。

ブロックで上記の逆進可能操作が呼び出されない限り、change_table も逆進可能です。

これ以外のメソッドを使う必要がある場合は、changeメソッドの代わりにreversibleメソッドを利用するか、updownメソッドを明示的に書いてください。

3.11 reversibleを使う

マイグレーションが複雑になると、Active Recordがマイグレーションのchangeを逆進できなくなることがあります。reversibleメソッドを使うと、マイグレーションを通常どおり実行する場合と逆進する場合の動作を以下のように明示的に指定できます。

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

上のマイグレーションはpriceカラムをstring型に変更し、マイグレーションが元に戻されるときにinteger型に戻します。direction.updirection.downにそれぞれブロックを渡していることにご注目ください。

または、changeの代わりに以下のようにupdownに分けて書いても同じことができます。

class ChangeProductsPrice < ActiveRecord::Migration[7.2]
  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

さらにreversibleは、生SQLクエリを実行するときや、Active Recordメソッドに直接相当するものがないデータベース操作を実行するときにも便利です。以下のように、reversibleで、マイグレーションを実行するときの操作や、マイグレーションを元に戻すときの操作を個別に指定できます。

class ExampleMigration < ActiveRecord::Migration[7.2]
  def change
    create_table :distributors do |t|
      t.string :zipcode
    end
    reversible do |direction|
      direction.up do
        # distributors_viewを作成する
        execute <<-SQL
          CREATE VIEW distributors_view AS
          SELECT id, zipcode
          FROM distributors;
        SQL
      end
      direction.down do
        execute <<-SQL
          DROP VIEW distributors_view;
        SQL
      end
    end

    add_column :users, :address, :string
  end
end

reversibleメソッドを使えば、指示が正しい順序で実行されることも保証されます。上のマイグレーション例を元に戻すと、users.addressカラムが削除された直後、distributorsテーブルが削除される直前にdownブロックが実行されます。

3.12 up/downメソッドを使う

changeの代わりに、従来のupメソッドとdownメソッドも利用できます。

upメソッドにはスキーマに対する変換方法を記述し、downメソッドにはupメソッドによって行われた変換をロールバック(逆進、取り消し)する方法を記述する必要があります。つまり、upの後にdownを実行した場合、スキーマが元通りになる必要があります。

たとえば、upメソッドでテーブルを作成したら、downメソッドではそのテーブルを削除する必要があります。downメソッド内で行なう変換の順序は、upメソッド内で行なう順序の正確な逆順にするのが賢明です。先のreversibleセクションの例は以下と同等になります。

class ExampleMigration < ActiveRecord::Migration[7.2]
  def up
    create_table :distributors do |t|
      t.string :zipcode
    end

    # distributors_viewを作成する
    execute <<-SQL
      CREATE VIEW distributors_view AS
      SELECT id, zipcode
      FROM distributors;
    SQL

    add_column :users, :address, :string
  end

  def down
    remove_column :users, :address

    execute <<-SQL
      DROP VIEW distributors_view;
    SQL

    drop_table :distributors
  end
end

3.13 逆進防止用のエラーを発生させる

場合によっては、逆進しようがないマイグレーションを実行することもあります(データの一部を削除するなど)。

このような場合、以下のようにdownブロックでActiveRecord::IrreversibleMigrationをraiseできます。

class IrreversibleMigrationExample < ActiveRecord::Migration[7.2]
  def up
    drop_table :example_table
  end

  def down
    raise ActiveRecord::IrreversibleMigration, "データ破棄マイグレーションなので逆進できません"
  end
end

誰かがマイグレーションを取り消そうとすると、逆進不可能であることを示すエラーメッセージが表示されます。

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

revertメソッドを使うと、Active Recordマイグレーションのロールバック機能を利用できます。

require_relative '20121212123456_example_migration'

class FixupExampleMigration < ActiveRecord::Migration[7.2]
  def change
    revert ExampleMigration

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

revertメソッドには、逆進を行う命令を含むブロックも渡せます。これは、以前のマイグレーションの一部のみを逆進させたい場合に便利です。

たとえば、ExampleMigrationがコミット済みになっており、後になってDistributorsビュー(データベースビュー)が不要になったとします。この場合、revertを使ってビューを削除するマイグレーションを作成できます。

class DontUseDistributorsViewMigration < ActiveRecord::Migration[7.2]
  def change
    revert do
      # ExampleMigrationのコードのコピペ
      create_table :distributors do |t|
        t.string :zipcode
      end

      reversible do |direction|
        direction.up do
          # distributors_viewを作成する
          execute <<-SQL
            CREATE VIEW distributors_view AS
            SELECT id, zipcode
            FROM distributors;
          SQL
        end
        direction.down do
          execute <<-SQL
            DROP VIEW distributors_view;
          SQL
        end
      end

      # 以後のマイグレーションはOK
    end
  end
end

revertを使わなくても同様のマイグレーションは自作できますが、その分以下の作業が増えます。

  1. create_tablereversibleの順序を逆にする。
  2. create_tabledrop_tableに置き換える。
  3. 最後にupdownを入れ替える。

revertは、これらの作業を一手に引き受けてくれます。

4 マイグレーションを実行する

Railsにはマイグレーションを実行するためのコマンドがいくつか用意されています。

マイグレーションを実行するrailsコマンドの筆頭といえば、rails db:migrateでしょう。このタスクは基本的に、まだ実行されていないchangeまたはupメソッドを実行します。未実行のマイグレーションがない場合は何もせずに終了します。マイグレーションの実行順序は、マイグレーションの日付が基準になります。

db:migrateタスクを実行すると、db:schema:dumpコマンドも同時に呼び出されます。このコマンドはdb/schema.rbスキーマファイルを更新し、スキーマがデータベースの構造に一致するようにします。

マイグレーションの特定バージョンを指定すると、Active Recordは指定されたマイグレーションに達するまでマイグレーション(changeupdown)を実行します。マイグレーションのバージョンは、マイグレーションファイル名冒頭の数字で表されます。たとえば、20240428000000というバージョンまでマイグレーションしたい場合は、以下を実行します。

$ bin/rails db:migrate VERSION=20240428000000

20240428000000というバージョンが現在のバージョンより大きい場合(新しい方に進む通常のマイグレーションなど)、20240428000000に到達するまで(このマイグレーション自身も実行対象に含まれます)のすべてのマイグレーションのchange(またはup)メソッドを実行し、その先のマイグレーションは行いません。過去に遡るマイグレーションの場合、20240428000000に到達するまでのすべてのマイグレーションのdownメソッドを実行しますが、上と異なり、20240428000000自身は含まれない点にご注意ください。

4.1 ロールバック

直前に行ったマイグレーションをロールバックして取り消す作業はよく発生します(マイグレーションに誤りがあって訂正したい場合など)。この場合、いちいちバージョン番号を調べて明示的にロールバックを実行しなくても、以下を実行するだけで済みます。

$ bin/rails db:rollback

これにより、changeメソッドを逆進実行するか、downメソッドを実行する形で直前のマイグレーションにロールバックします。マイグレーションを2つ以上ロールバックしたい場合は、STEPパラメータを指定できます。

$ bin/rails db:rollback STEP=3

これにより、最後に行った3つのマイグレーションがロールバックされます。

ローカルのマイグレーションを一時的に変更して、再度マイグレーションする前にその特定のマイグレーションをロールバックしたい場合には、db:migrate:redoコマンドが使えます。db:rollbackコマンドと同様に、複数のバージョンを戻す必要がある場合はSTEPパラメータを指定できます。

$ bin/rails db:migrate:redo STEP=3

db:migrateコマンドでも、db:migrate:redoコマンドと同じ結果を得られます。ただしdb:migrate:redoコマンドは利便性のために用意されており、マイグレーション先のバージョンを明示的に指定する必要はありません。

4.1.1 トランザクション

DDLトランザクションをサポートするデータベースでは、単一のトランザクションでスキーマを変更すると、個別のマイグレーションがトランザクションにラップされます。

マイグレーションが途中で失敗した場合、途中まで正常に適用された変更はトランザクションによってすべてロールバックされ、データベースの一貫性が維持されます。つまり、トランザクション内のあらゆる操作は「正常に実行される」か「まったく実行されない」かのどちらかだけになり、トランザクションの途中でエラーが発生したときにデータベースが不整合な状態になるのを防ぎます。

データベースがDDLトランザクション(スキーマを変更するステートメントなど)をサポートしていない場合、マイグレーションが失敗しても成功した部分はロールバックされません。この変更は手動でロールバックする必要があります。

ただし、ある種のクエリはトランザクション内では実行できないので、そのような状況では、以下のようにdisable_ddl_transaction!で自動トランザクションを意図的にオフにできます。

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

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

self.disable_ddl_transaction!でマイグレーションしている場合でも、独自のトランザクションを別途オープンすることは可能である点にご注意ください。

4.2 データベースをセットアップする

bin/rails db:setupコマンドは、「データベースの作成」「スキーマの読み込み」「seedデータを用いたデータベースの初期化」をまとめて実行します。

4.3 データベースを準備する

bin/rails db:prepareコマンドはbin/rails db:setupに似ていますが、冪等(べきとう: idempotent)に振る舞うので、複数回呼び出しても問題が生じず、必要なタスクは1回だけ実行されます。

  • データベースがまだ作成されていない場合: bin/rails db:prepareコマンドはbin/rails db:setupと同じように実行されます。

  • データベースは存在するがテーブルが作成されていない場合: bin/rails db:prepareコマンドはスキーマを読み込んで保留中のマイグレーションを実行し、更新されたスキーマをダンプし、最後にseedデータを読み込みます。詳しくは、seedデータのセクションを参照してください。

  • データベースとテーブルの両方が存在するがseedデータが読み込まれていない場合: bin/rails db:prepareコマンドはseedデータのみを読み込みます。

  • データベース、テーブル、seedデータがすべて揃っている場合: bin/rails db:prepareコマンドは何も行いません。

データベースの作成、テーブルの作成、seedデータの読み込みがすべて完了した後は、読み込み済みのseedデータや既存のseedファイルを変更または削除しても、このコマンドではseedデータの再読み込みは行われません。seedデータを再度読み込むには、bin/rails db:seedを手動で実行してください。

4.4 データベースをリセットする

bin/rails db:resetコマンドは、データベースをdropして再度設定します。このコマンドはrails db:drop db:setupと同等です。

このコマンドは、すべてのマイグレーションを実行することと等価ではありません。このコマンドは単に現在のschema.rbの内容をそのまま使い回します。マイグレーションをロールバックできなくなると、rails db:resetを実行しても復旧できないことがあります。スキーマダンプについて詳しくは、スキーマダンプの意義セクションを参照してください。

4.5 特定のマイグレーションのみを実行する

特定のマイグレーションをupまたはdown方向に実行する必要がある場合は、db:migrate:upまたはdb:migrate:downタスクを使います。以下に示したように、適切なバージョン番号を指定するだけで、該当するマイグレーションに含まれるchangeupdownメソッドのいずれかが呼び出されます。

$ bin/rails db:migrate:up VERSION=20240428000000

上を実行すると、バージョン番号が20240428000000のマイグレーションに含まれるchangeメソッド(またはupメソッド)が実行されます。

このコマンドは、最初にそのマイグレーションが実行済みであるかどうかをチェックし、Active Recordによって実行済みであると認定された場合は何も行いません。

指定のバージョンが存在しない場合は、以下のように例外を発生します。

$ bin/rails db:migrate VERSION=00000000000000
rails aborted!
ActiveRecord::UnknownMigrationVersionError:

No migration with version number 00000000000000.

4.6 環境を指定してマイグレーションを実行する

デフォルトでは、rails db:migratedevelopment環境で実行されます。 他の環境に対してマイグレーションを行いたい場合は、コマンド実行時にRAILS_ENV環境変数を指定します。たとえば、test環境でマイグレーションを実行する場合は以下のようにします。

$ bin/rails db:migrate RAILS_ENV=test

4.7 マイグレーション実行結果の出力を変更する

デフォルトでは、マイグレーション実行後に正確な実行内容とそれぞれの所要時間が出力されます。 たとえば、テーブル作成とインデックス追加を行なうと次のような出力が得られます。

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

マイグレーションには、これらの出力方法を制御するためのメソッドが提供されています。

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

以下のマイグレーションを例に説明します。

class CreateProducts < ActiveRecord::Migration[7.2]
  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 Recordから何も出力したくない場合は、bin/rails db:migrate VERBOSE=falseで出力を完全に抑制できます。

4.8 Railsのマイグレーションによるバージョン管理

Railsは、データベースのschema_migrationsテーブルを介して、どのマイグレーションが実行されたかをトラッキングします。マイグレーションを実行すると、Railsはversionカラムに保存されているマイグレーションのバージョン番号を含む行をschema_migrationsテーブルに挿入します。これにより、Railsはどのマイグレーションがデータベースに適用済みかを判断できます。

たとえば、20240428000000_create_users.rbという名前のマイグレーションファイルがある場合、Railsはこのファイル名からバージョン番号(20240428000000)を抽出し、マイグレーションが正常に実行された後にそれをschema_migrationsテーブルに挿入します。

schema_migrationsテーブルの内容は、データベース管理ツールで直接表示することも、以下のようにRailsコンソールで表示することも可能です。

rails dbconsole

次に、データベースコンソール内で以下のようにschema_migrationsテーブルにクエリできます。

SELECT * FROM schema_migrations;

これにより、データベースに適用されたすべてのマイグレーションのバージョン番号のリストが表示されます。Railsはこの情報を用いて、rails db:migrateコマンドやrails db:migrate:upコマンドでどのマイグレーションを実行する必要があるかを決定します。

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

マイグレーションを自作していると、ときにはミスしてしまうこともあります。いったんマイグレーションを実行してしまった後では、既存のマイグレーションを単に編集してもう一度マイグレーションをやり直しても意味がありません。Railsはそのマイグレーションが既に実行済みであると認識しているので、rails db:migrateを実行しただけでは何も変更されません。 このような場合には、マイグレーションをいったんロールバック(rails db:rollbackなど)してからマイグレーションを修正し、それからbin/rails db:migrateを実行して修正済みバージョンのマイグレーションを実行する必要があります。

一般に、Gitなどのバージョン管理システムに既にコミットされた既存のマイグレーションを直接書き換えるのはよくありません。既存のマイグレーションが既にproduction環境で運用されているときに既存のマイグレーションを書き換えると、自分自身はもちろん、共同作業者も余分な作業を強いられます。 既存のマイグレーションを書き換えるのではなく、必要な変更を実行する新しいマイグレーションを作成すべきです。

なお、マイグレーションを新しく作成した直後で、バージョン管理システムにまだコミットしていない(一般的に言えば、開発用のコンピュータ以外に反映されていない)のであれば、そうしたマイグレーションを書き換えることは普通に行われています。

revertメソッドは、以前のマイグレーション全体またはその一部を逆進させるためのマイグレーションを新たに書くときにも便利です(上述の以前のマイグレーションに戻すを参照してください)。

6 スキーマダンプの意義

6.1 スキーマファイルの意味について

Railsのマイグレーションは強力ではありますが、データベースのスキーマを作成するための信頼できる情報源ではありません。最終的に信頼できる情報源は、やはり現在動いているデータベースです

Railsは、データベーススキーマの最新の状態のキャプチャを試みるために、デフォルトでdb/schema.rbファイルを生成します。

アプリケーションのデータベースの新しいインスタンスを作成する場合、マイグレーションの全履歴を最初から繰り返すよりも、単にrails db:schema:loadでスキーマファイルを読み込む方が、高速でエラーも起きにくい傾向があります。

マイグレーション内の外部依存性が変更されたり、マイグレーションと異なる進化を遂げたアプリケーションコードに依存していたりすると、古いマイグレーションを正しく適用できなくなる可能性があります。

スキーマファイルは、Active Recordの現在のオブジェクトにある属性を手軽にチェックするときにも便利です。スキーマ情報はモデルのコードにはありません。スキーマ情報は多くのマイグレーションに分かれて存在しており、そのままでは非常に探しにくいものですが、こうした情報はスキーマファイルにコンパクトな形で保存されています。

6.2 スキーマダンプの種類

Railsで生成されるスキーマダンプのフォーマットは、config/application.rbで定義されるconfig.active_record.schema_format設定で制御されます。デフォルトのフォーマットは:rubyですが、:sqlも指定できます。

6.2.1 デフォルトの:rubyスキーマを利用する場合

:rubyを選択すると、スキーマはdb/schema.rbに保存されます。このファイルを開いてみると、1つの巨大なマイグレーションのように見えます。

ActiveRecord::Schema[7.2].define(version: 2008_09_06_171750) 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などでその構造を表現することで作成されています。

6.2.2 :sqlスキーマダンプを利用する場合

しかし、db/schema.rbでは、「トリガ」「シーケンス」「ストアドプロシージャ」「チェック制約」などのデータベース固有の項目までは表現できません。

マイグレーションでexecuteを用いれば、RubyマイグレーションDSLでサポートされないデータベース構造も作成できますが、そうしたステートメントはスキーマダンプで再構成されない点にご注意ください。

これらの機能が必要な場合は、新しいデータベースインスタンスの作成に有用なスキーマファイルを正確に得るために、スキーマのフォーマットに:sqlを指定する必要があります。

スキーマフォーマットを:sqlにすると、データベース固有のツールを用いてデータベースの構造をdb/structure.sqlにダンプします。たとえばPostgreSQLの場合はpg_dumpユーティリティが使われます。MySQLやMariaDBの場合は、多くのテーブルでSHOW CREATE TABLEの出力結果がファイルに含まれます。

スキーマをdb/structure.sqlから読み込む場合、bin/rails db:schema:loadを実行します。これにより、含まれているSQL文が実行されてファイルが読み込まれます。定義上、これによって作成されるデータベース構造は元の完全なコピーとなります。

6.3 スキーマダンプとソースコード管理

スキーマダンプは一般にデータベースの作成に使われるものなので、スキーマファイルはGitなどのソースコード管理の対象に加えておくことを強く推奨します。

複数のブランチでスキーマを変更すると、マージしたときにスキーマファイルがコンフリクトする可能性があります。 コンフリクトを解決するには、bin/rails db:migrateを実行してスキーマファイルを再生成してください。

新規生成されたRailsアプリでは、既にmigrationsフォルダがgitツリーに含まれているので、必要な作業は、新たに追加するマイグレーションを常にgitに追加してコミットすることだけです。

7 Active Recordと参照整合性

Active Recordパターンでは、「高度な処理は、基本的にデータベースよりもモデル側に配置すべき」であることを示唆しています。したがって、高度な処理の一部をデータベース側で行うトリガーや制約などの機能は、設計理念としては無条件に望ましいとは限りません。

validates :foreign_key, uniqueness: trueのようなデータベースバリデーション機能は、データ整合性をモデルが強制する方法の1つです。モデルで関連付けの:dependentオプションを指定すると、親オブジェクトが削除されたときに子オブジェクトも自動的に削除されます。アプリケーションレベルで実行される他の機能と同様、モデルのこうした機能だけでは参照整合性を維持できないため、開発者によってはデータベースの外部キー制約機能を用いて参照整合性を補強することもあります。

しかし現実には、外部キー制約とuniqueインデックスについては一般にデータベースレベルで適用する方が安全であると考えられます。Active Recordは、このようなデータベースレベルの機能の操作を直接サポートしていませんが、executeメソッドを使えば任意のSQLコマンドを実行可能です。

Active Recordパターンは高度な処理をモデル側に配置することを重視していますが、外部キーやunique制約についてはデータベースレベルで実装しておかないと整合性の問題が発生する可能性があることは、ここで強調しておく価値があります。したがって、必要に応じてActive Recordパターンにデータベースレベルの制約を併用する形で機能を補完しておくことをオススメします。こうしたデータベースレベルの制約を使う場合は、そうした制約に対応する関連付けやバリデーションをコード内でも明示的に定義しておき、アプリケーションレイヤとデータベースレイヤの双方でデータの整合性を確保しておくべきです。

8 マイグレーションとseedデータ

Railsのマイグレーション機能の主要な目的は、スキーマ変更のコマンドを一貫した手順で発行できるようにすることですが、データの追加や変更にも利用できます。これは、productionのデータベースのような削除や再作成を行えない既存データベースで便利です。

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

  def down
    Product.delete_all
  end
end

Railsには、データベース作成後に初期データを素早く簡単に追加するシード(seed)機能があります。seedは、development環境やtest環境で頻繁にデータを再読み込みする場合に特に便利です。

seed機能を使うには、db/seeds.rbを開いてRubyコードを記述し、rails db:seedを実行します。

seedに記述するコードは、いつ、どの環境でも実行できるように冪等にしておくべきです。

["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
  MovieGenre.find_or_create_by!(name: genre_name)
end

この方法なら、マイグレーションよりもずっとクリーンに空のアプリケーションのデータベースをセットアップできます。

9 古いマイグレーション

db/schema.rbdb/structure.sqlは、使っているデータベースの最新ステートのスナップショットであり、そのデータベースを再構築するための情報源として信頼できます。これを手がかりにして、古いマイグレーションファイルを削除・削減できます。

db/migrate/ディレクトリ内のマイグレーションファイルを削除しても、マイグレーションファイルが存在していたときにrails db:migrateが実行されたあらゆる環境は、Rails内部のschema_migrationsという名前のデータベース内に保存されている(マイグレーションファイル固有の)マイグレーションタイムスタンプへの参照を保持し続けます。詳しくはマイグレーションによるバージョン管理セクションを参照してください。

マイグレーションファイルを削除した状態でrails db:migrate:statusコマンド(本来マイグレーションのステータス(upまたはdown)を表示する)を実行すると、削除したマイグレーションファイルの後に********** NO FILE **********と表示されるでしょう。これは、そのマイグレーションファイルが特定の環境で一度実行されたが、db/migrate/ディレクトリの下に見当たらない場合に表示されます。

9.1 Railsエンジンからのマイグレーション

Railsエンジンでマイグレーションを行う場合、注意すべき点があります。エンジンでのマイグレーションをインストールするrakeタスクは「冪等」であり、2回以上実行しても結果が変わりません。 つまり、親アプリケーションに存在するマイグレーションは、以前のインストールによってスキップされ、マイグレーションが見つからない場合は最新のタイムスタンプでコピーされます。 古いエンジンのマイグレーションを削除してからインストールタスクを再実行すると、新しいタイムスタンプを持つ新しいファイルが作成され、db:migrateはそれらの新しいファイルを再実行しようとします。

したがって、一般にエンジン由来のマイグレーションは変更されないよう保護しておきましょう。そのようなマイグレーションには以下のような特殊なコメントがあります。

# This migration comes from blorgh (originally 20210621082949)

10 その他

10.1 IDの代わりにUUIDを主キーに使う場合

デフォルトのRailsは、オートインクリメントされる整数値をデータベースレコードの主キーとして利用します。ただし、分散システムや外部サービスとの統合が必要な場合など、主キーとしてUUID(Universally Unique Identifier: 汎用一意識別子)を使う方が有利になるシナリオもあります。UUIDは、IDを生成する中央機関に依存せずに、グローバルで一意な識別子を提供します。

10.1.1 RailsでUUIDを有効にする

RailsアプリケーションでUUIDを利用する前に、データベースがUUIDの保存をサポートしていることを確認しておく必要があります。さらに、UUIDを処理できるようにデータベースアダプタを構成しておく必要が生じる場合もあります。

バージョン13より前のPostgreSQLを使っている場合は、gen_random_uuid()関数にアクセスするためにpgcrypto拡張機能を有効にしておく必要があるでしょう。

  1. Railsを設定する

    Railsのアプリケーション設定ファイル(config/application.rb)に以下の行を追加して、RailsがデフォルトでUUIDを主キーとして生成するように構成します。

    config.generators do |g|
      g.orm :active_record, primary_key_type: :uuid
    end
    

    この設定によって、Active Recordモデルのデフォルトの主キーにUUIDを使うようRailsに指示します。

  2. UUIDで参照を追加する

    モデル間の関連付けを参照で作成するときは、主キーの種別との一貫性を維持するために、以下のようにデータ型を:uuidとして指定します。

    create_table :posts, id: :uuid do |t|
      t.references :author, type: :uuid, foreign_key: true
      # 他のカラム...
      t.timestamps
    end
    

    この例では、postsテーブルのauthor_idカラムはauthorsテーブルのidカラムを参照しています。主キーの種別を明示的に:uuidに設定することで、外部キーカラムが参照する主キーのデータ型と一致することが保証されます。他の関連付けやデータベースに合わせて構文を調整してください。

  3. マイグレーションが変更される

    以下のコマンドでモデルのマイグレーションを生成すると、iduuid:型になっていることがわかります。

      $ bin/rails g migration CreateAuthors
    
    class CreateAuthors < ActiveRecord::Migration[7.2]
      def change
        create_table :authors, id: :uuid do |t|
          t.timestamps
        end
      end
    end
    

    上のマイグレーションによって以下のスキーマが生成されます。

    create_table "authors", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
      t.datetime "created_at", precision: 6, null: false
      t.datetime "updated_at", precision: 6, null: false
    end
    

    このマイグレーションのidカラムは、gen_random_uuid()関数によって生成されるデフォルト値を持つUUID主キーとして定義されます。

UUIDは、異なるシステム間でグローバルに一意であることが保証されているため、分散アーキテクチャに適しています。また、集中ID生成に依存しない一意の識別子を提供することで、外部システムやAPIとの統合をシンプルにできます。また、オートインクリメントの整数値とは異なり、UUIDはテーブル内のレコードの合計数に関する情報が公開されないため、セキュリティ上の利点もあります。

ただしUUIDは通常のIDよりもサイズが大きいため、パフォーマンスに影響する可能性があり、インデックス作成がより困難になります。UUIDは、整数の主キーや外部キーと比較すると、書き込みや読み取りのパフォーマンスが低下します。

したがって、UUIDを主キーに採用することを決定する前に、こうしたトレードオフを評価し、アプリケーション固有の要件を考慮することが重要です。

10.2 データのマイグレーション

データをマイグレーションすると、データベース内でデータが変換されたり移動したりします。Railsでは一般的に、マイグレーションファイルでデータを操作することは推奨されません。理由は以下のとおりです。

  • 関心の分離: スキーマ変更とデータ変更は、ライフサイクルも目的もそれぞれ異なります。スキーマ変更はデータベースの構造を変更するための操作ですが、データ変更はコンテンツを書き換える操作です。
  • ロールバックが複雑になる: データのマイグレーションを安全かつ予測通りにロールバックすることは困難になる場合があります。
  • パフォーマンス: データのマイグレーションは実行に長時間かかり、完了するまでテーブルがロックされてアプリケーションのパフォーマンスと可用性に悪影響が生じる可能性があります。

データをマイグレーションで操作するよりも、maintenance_tasks gemの利用を検討してください。このgemは、スキーマのマイグレーションを妨げることなく、安全かつ簡単に管理できる方法でデータのマイグレーションやその他のメンテナンスタスクを作成・管理するためのフレームワークを提供します。

フィードバックについて

Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。

原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨

本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。

Railsガイド運営チーム (@RailsGuidesJP)

支援・協賛

Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。

  1. Star
  2. このエントリーをはてなブックマークに追加