[Rails]マイグレーションマスターに俺はなる！

# はじめに
開発している際に、マイグレーションファイルを操作することがあるかと思いますが、操作頻度の割に知識が少ないんじゃ無いかな？と不安になったので調べてみました。
この記事を見れば、かなりRails開発でのマイグレーション操作に強くなるかと思います！

# マイグレーションの概要
マイグレーションは、`データベーススキーマの継続的な変更(英語)を、統一的かつ簡単に行なう`ための手法です。
マイグレーションではRubyのDSLを使っているので、生のSQLを作成する必要がなく、スキーマとスキーマへの変更をデータベースの種類に依存せずに済みます。

1つ1つのマイグレーションは、データベースの新しい'version'とみなすことができます。
スキーマは空の状態から、マイグレーションを実行することで変更を加えていきます。
`ActiveRecordはdb/schema.rbファイルを更新し、データベースの最新の構造と一致`するようにします。

# マイグレーションの作成
マイグレーションを作成するには以下のコマンドを実行します。
マイグレーションファイル名は`YYYYMMDDHHMMSS_create_products.rb`のような形式になります。

```ruby
$ rails g migration AddPriceToProducts
#=> 20210516120000_add_price_to_products.rb

# カラムとデータ型を指定
$ rails g migration AddPariceProducts pricer:integer

# 複数指定も可能
$ rails g migration AddPariceProducts pricer:integer name:string

# priceカラムを削除
$ rails g migration RemovePriceToProducts
#=> 20210516120000_remove_price_to_products.rb
```

## モデルの作成
モデルのジェネレータとscaffoldジェネレータは、新しいモデルを追加するマイグレーションを生成します。このマイグレーションには、関連するテーブルを作成する命令が既に含まれています。必要なカラムを指定すると、それらのカラムを追加する命令も同時に生成されます。

```ruby
$ rails g model Product name:string
#=> 20210516120000_create_products.rb

# データ型のデフォルトはstringなので省略可能
$ rails g model Product name
#=> 20210516120000_create_products.rb
```

コマンドを実行すると以下のようなファイルが生成される。

```ruby
class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    create_table :products do |t|
      # nameカラム作成
      t.string :name

      # created_atカラム、updated_atカラム作成
      t.timestamps
    end
  end
end
```

# マイグレーションの自作
コマンドで簡単に作ることは出来ますが、自作で自由にマイグレーションファイルを作成することも可能です。

## テーブルの作成
テーブルを作成する時は、`create_table`メソッドを使用します。

```ruby
# productsテーブルを作成
create_table :products do |t|
  # nameカラム作成
  t.string :name
end
```

## テーブル結合を作成
has_and_belongs_to_manyテーブル結合（join）を作成する際は、`create_join_table`メソッドを使用します。
指定したテーブルの`モデル名_id`は自動で定義されます。

```ruby
# 引数の最初の2つをつなげたものが結合し、categories_productsテーブルを作成
create_join_table :products, :categories do |t|
  # product_idとcategory_idにインデックスを追加
  t.index :product_id
  t.index :category_id
end
```

## テーブルの変更
既存のテーブルを変更するには、`change_table`メソッドを使用します。
基本的にはcreate_tableメソッドと同じ要領で使います。

```ruby
change_table :products do |t|
  # price、nameカラムを削除
  t.remove :price, :name

  # products_numberカラムを生成、インデックスを追加
  t.string :products_number
  t.index :products_number

  # upccodeカラムを、upc_codeにカラム名を変更
  t.rename :upccode, :upc_code
end
```

## カラムの操作

### カラムの追加
`add_column`で指定したカラムを追加することが出来ます。

```ruby
class AddPriceToProducts < ActiveRecord::Migration[5.0]
  def change
    # Productsテーブルにpriceカラムを追加
    add_column :products, :price, :integer
  end
end
```

### カラムの削除
`remove_column`では逆に指定したカラムを削除することが出来ます。

```ruby
class RemovePriceFromProducts < ActiveRecord::Migration[5.0]
  def change
    # Productsテーブルのpriceカラムを削除
    remove_column :products, :price, :integer
  end
end
```

### カラムの変更
`change_column`ではカラムのデータ型を変更することが出来ます。

```ruby
# productsテーブルのproducts_numberカラムをtext型に変更
change_column :products, :products_number, :text

# productsテーブルのnameカラムのNotNull制約をfalseにする
change_column_null :products, :name, false

# productsテーブルのapprovedカラムのデフォルト値を指定
change_column_default :products, :approved, from: true, to: false
```

## カラム修飾子

| 修飾子 | 意味 |
|:-:|:-:|
| limit  | 最大サイズを設定  |
| precision  | decimalフィールドの精度 (precision) を定義  |
| scale  | decimalフィールドの精度 (スケール: scale) を指定  |
| polymorphic  | belongs_to関連付けで使うtypeカラムを追加  |
| null  | trueでNULL値を許可、falseで禁止  |
| default  | カラムでのデフォルト値の設定  |
| comment  | カラムにコメントを追加  |

## 外部キー
`add_foreign_key`メソッドで外部キー制約を追加することができます。
Railsでは、すべての外部キーはfk_rails_という名前で始まり、その後ろにfrom_tableとcolumnから一意に生成された文字列が10文字追加されます。 必要であれば、:nameオプションを指定することで別の名前を使えます。

```ruby
# articlesテーブルのauthor_idカラムを追加
add_foreign_key :articles, :authors

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

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

# 名前を指定して外部キーを削除する場合
remove_foreign_key :accounts, name: :special_fk_name
```

## 通常とは逆の動作を指定
マイグレーションを通常どおり実行する場合と逆転する場合の動作を指定する際は、`reversible`メソッドを使用します。

```ruby
class ExampleMigration < ActiveRecord::Migration[5.0]
  def change
    create_table :distributors do |t|
      t.string :zipcode
    end

    reversible do |dir|
      dir.up do
        # CHECK制約を追加
        execute <<-SQL
          ALTER TABLE distributors
            ADD CONSTRAINT zipchk
              CHECK (char_length(zipcode) = 5) NO INHERIT;
        SQL
      end
      dir.down do
        execute <<-SQL
          ALTER TABLE distributors
            DROP CONSTRAINT zipchk
        SQL
      end
    end

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

## up/downメソッド
changeの代りに、従来のupメソッドとdownメソッドを使うこともできます。
upメソッドにはスキーマに対する変換方法を記述し、downメソッドにはupメソッドによって行われた変換を逆転する方法を記述する必要があります。つまり、upの後にdownを実行した場合、スキーマが変更されないようにする必要があります。 たとえば、`upメソッドでテーブルを作成したら、downメソッドではそのテーブルを削除する`必要があります。
downメソッド内では、upメソッド内で行なうのとは逆順にするのが主流です。

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

    # CHECK制約を追加
    execute <<-SQL
      ALTER TABLE distributors
        ADD CONSTRAINT zipchk
        CHECK (char_length(zipcode) = 5);
    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 distributors
        DROP CONSTRAINT zipchk
    SQL

    drop_table :distributors
  end
end
```

# マイグレーションの実行
主にマイグレーションを実行するrailsコマンドで使うのは、ほとんどの場合`rails db:migrate`コマンドだと思います。現在、実行されていないchangeまたはupメソッドを実行します。マイグレーションの実行順序は、マイグレーションの日付に基づきます。

```bash
# 通常のマイグレーション実行
$ rails db:migrate

# バージョン(マイグレーションファイル名の冒頭に付いている数字)指定も可能
$ rails db:migrate VERSION=20080906120000
```

## ロールバック
マイグレーションがロールバックを実行するには、`rails db:rollback`を実行します。
changeメソッドを逆転実行するかdownメソッドを実行します。

```bash
# 直前のマイグレーションがロールバック
$ rails db:rollback

# 最後から指定した数だけロールバックする
$ rails db:rollback STEP=3

# ロールバックと再マイグレーションを一度に実行
$ rails db:migrate:redo
```

## データベースの設定
`rails db:setup`コマンドを実行することで、データベースの作成、スキーマの読み込み、シードデータを用いてデータベースの初期化を実行します。

## データベースをリセットする
`rails db:reset`コマンドは、データベースをdropして再設定します。
このコマンドは、すべてのマイグレーションを実行することと同じ動作ではありません。このコマンドでは現在のschema.rbの内容をそのまま使い回しています。

## 特定のマイグレーションの実行
特定のマイグレーションを実行する場合は、`rails db:migrate:up`または`rails db:migrate:down`を実行します。該当するマイグレーションに含まれるchange、up、downメソッドのいずれかが呼び出されます。

```bash
# up方向にマイグレーションを実行
$ rails db:migrate:up

# down方向にマイグレーションを実行
$ rails db:migrate:down

# バージョン指定して実行
$ rails db:migrate:up VERSION=20080906120000
```

## 異なる環境でマイグレーションを実行
デフォルトでは、development環境でマイグレーションは実行されます。 
別の環境に対してマイグレーションを行いたい場合は、コマンド実行時にRAILS_ENV環境変数を指定します。

```bash
# test環境でマイグレーション実行
$ rails db:migrate RAILS_ENV=test
```

## マイグレーション実行結果の出力変更
デフォルトでは、マイグレーション実行後に正確な実行内容とそれぞれの所要時間が出力されます。 
マイグレーションには、これらの出力方法を制御するためのメソッドが提供されています。

| メソッド | 目的 |
|:-:|:-:|
| suppress_messages  | 引数としてブロックを1つ取り、そのブロックでの出力をすべて抑制  |
| say | 引数としてメッセージを1つ受け取り、それをそのまま出力  |
| say_with_time  | 受け取ったブロックを実行するのにかかった時間を示すテキストを出力  |

```ruby
class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    # do 〜 end内の出力を抑制
    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 }

    # 指定したメッセージを出力し、出力をインデントするためにtrueを渡す
    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) =======================================
```

# 既存のマイグレーションの変更
マイグレーションを自作していると、ときにはミスしてしまうこともあります。Railsでは既存のマイグレーションを直接変更するのは一般的に良しとされていません。
こういった場合は、既存のマイグレーションを直接修正せず、修正用のマイグレーションを新たに作成してそれを実行するのが正しい方法です。
`revert`メソッドは、以前のマイグレーション全体またはその一部を取り消すためのマイグレーションを新たに書くときにも便利です。

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

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

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

```db/schema.rb
ActiveRecord::Schema.define(version: 2021_05_16_123456) do
  create_table "users", 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.integer     "price"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.string.  "products_number"
  end
end
```

db/schema.rbでは、トリガ/シーケンス/ストアドプロシージャ/チェック制約などのデータベース固有の項目を表現できません。これらの機能が必要な場合は、新しいデータベースインスタンスの作成に有用なスキーマファイルを正確に得るためにスキーマのフォーマットを:sqlにする必要があります。

スキーマフォーマットを:sqlにすると、データベース固有のツールを用いてデータベースの構造を`db/structure.sql`にダンプします。

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

# Active Recordと参照整合性
Active Recordは、「知的に振る舞うのはモデルであり、データベースではない」というコンセプトに基づいています。

`validates :foreign_key`や`uniqueness: true`のようなデータベース検証機能は、データ整合性の強制をモデルが行っている例です。

Active Recordだけではこうした外部機能を扱うツールをすべて提供することはできませんが、`execute`メソッドを使えば任意のSQLを実行できます。

# マイグレーションとシードデータ
Railsには、データベース作成後に初期データを追加するためのseed機能があります。
シードは、development環境やtest環境で頻繁にデータを再読込する場合に特に便利です。シード機能は、db/seeds.rbにRubyコードを記述して`rails db:seed`を実行するだけ使用出来ます。

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

productionのデータベースのような削除や再作成を行えない既存データベースでは、以下のようにデータの追加や変更をすると便利です。

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

  def down
    Product.delete_all
  end
end
```

# 古いマイグレーション
`db/schema.rb`や`db/structure.sql`は、使っているデータベースの最新ステートのスナップショットであり、そのデータベースを再構築するための情報源として信頼できます。

db/migrate/ディレクトリ内のマイグレーションファイルを削除しても、Rails内部のschema_migrationsという名前のデータベース内に保存されている（マイグレーションファイル固有の）マイグレーションタイムスタンプへの参照を保持し続けます。

マイグレーションファイルを削除した状態で`rails db:migrate:status`コマンドでマイグレーションファイルの状態を確認すると、削除したマイグレーションファイルの後に`********** NO FILE **********`と表示されているかと思います。
マイグレーションファイルが特定の環境で一度実行されたが、db/migrate/配下に見当たらない場合に表示されます。

# 終わりに
マイグレーションを操作するには、色んな動作が出来ますね！
その分使いこなすのは難しいかもしれませんが、使っていけば徐々に理解していくことが出来ると思います。
よく使う操作ではあると思うので、頑張って習得しましょう！