Node.jsのSequelizeでDBのmigrationを実行する

Node.jsのORMである `Sequelize` はその基となったActiveRecordと同様にDBのマイグレーション機構も備わっています。 `Sequelize` のマイグレーション操作についてはまとまった記事が見当たらなかったので後世のために設定から基本操作までここに記しておこうと思います。
ここでは接続先のDBとしてPostgreSQLを利用していますが、MySQL、SQLiteでも同じ操作でmigrationが実行できます。

* 実行環境・ライブラリのバージョン情報

```
node: 7.5.0
sequelize: 3.30.2
sequelize-cli: 2.5.1
```

## 追記(2017年2月19日)
* [インデックスの追加](#add_index)
* [複合主キーの記述方法](#composite_primary_key)
* [コマンドまとめ](#command_list)

## DB接続の準備

まず、動作確認用のプロジェクトを作成します。

```bash
mkdir migration-test
cd migration-test
npm init
```

`sequelize` 本体と `PostgreSQL` を扱うためのライブラリをインストールします。

```bash
npm install sequelize --save
npm install pg --save
```

## コマンドライン実行のセットアップ

コマンドラインから `sequelize` のmigrationを実行できるように `sequelize-cli` をインストールします。

```bash
npm install sequelize-cli --save
```

以下のようにプロジェクトにインストールされた `sequelize` シェルを実行します。

```bash
node_modules/.bin/sequelize -v
```

グローバルインストールした場合は以下のように `sequelize` だけで実行できます。

```bash
npm install sequelize-cli -g
```

```bash
sequelize -v
```

## DB接続設定

以下のコマンドで `config` ディレクトリとその直下に `config.json` が生成されます。
sequelizeでのmigration実行では `config.json` の設定を使用してDBに接続します。

```bash
node_modules/.bin/sequelize init
```

`config.json` は生成直後は以下の状態です。
`host` `username` `password` をファイルに直接記述するのは避けたいところです。
ちなみに `dialect` は利用するDBの種類になります。

```json
{
  "development": {
    "username": "root",
    "password": null,
    "database": "database_development",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "test": {
    "username": "root",
    "password": null,
    "database": "database_test",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "production": {
    "username": "root",
    "password": null,
    "database": "database_production",
    "host": "127.0.0.1",
    "dialect": "mysql"
  }
}
```

DB接続設定は環境依存情報なので環境変数から取得するのがセオリーです。
`config.json` を以下のように書き換えます。
簡略化するために `test` 環境の設定を省きましたが必要ならば追加してください。　
唐突に出現した `use_env_variable` が気になる人は [こちら](https://github.com/sequelize/cli/blob/master/lib/tasks/db.js#L340) のコードを追ってみてください。
`DATABASE_URL` のところは任意の文字列でよいのですが、[Heroku環境にデプロイする際に都合がよい](https://devcenter.heroku.com/changelog-items/438) ので `DATABASE_URL` にしています。

```json
{
  "development": {
    "use_env_variable": "DATABASE_URL"
  },
  "production": {
    "use_env_variable": "DATABASE_URL"
  }
}
```

環境変数に `DATABASE_URL` を追加します。
PostgreSQLの場合の `DATABASE_URL` の書式は以下のようになります。

```
[DATABASE_URL]
postgres://[user_name]:[password]@[host]:[port]/[database_name]
```

|設定|値|
|:-:|:-:|
|host|localhost|
|user_name|user01|
|password|qazwsx|
|port|5432|
|database_name|dev|

接続設定が上記の値ならば `DATABASE_URL` は以下のようになります。

```
[DATABASE_URL]
postgres://user01:qazwsx@localhost:5432/dev
```

`export` コマンドで環境変数として登録します。

```bash
export DATABASE_URL=postgres://user01:qazwsx@localhost:5432/dev
```

## 新規テーブルの作成

### 雛型ファイルの生成

テーブル生成のための雛型は `sequelize model:create` というコマンドで生成できます。
以下のようにオプションとして `--name` `--attributes` を指定すると素敵です。
カラム名をスネークケースにする場合は自動付加されるタイムスタンプ列のために `--underscored` を指定します。 `--underscored` を指定しない場合は `createdAt` `updatedAt` というカラム名になります。

```bash
node_modules/.bin/sequelize model:create --name user --underscored --attributes name:string,birth:date,country_code:integer
```

`migrations` フォルダ下に `(実行日時)-create-user.js` というファイル名のJavaScriptファイルが生成されます。 `Ruby on Rails` に慣れている人なら見覚えのある形式だと気づくかもしれません。
必要に応じて `allowNull` や `default` 値を設定します。

```js
'use strict';
module.exports = {
  up: function(queryInterface, Sequelize) {
    return queryInterface.createTable('users', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      name: {
        type: Sequelize.STRING
      },
      birth: {
        type: Sequelize.DATE
      },
      country_code: {
        type: Sequelize.INTEGER
      },
      created_at: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updated_at: {
        allowNull: false,
        type: Sequelize.DATE
      }
    });
  },
  down: function(queryInterface, Sequelize) {
    return queryInterface.dropTable('users');
  }
};
```

`models` フォルダ下には `user.js` が生成されます。こちらはアプリケーション実行時に利用されるものです。

```js
'use strict';
module.exports = function(sequelize, DataTypes) {
  var User = sequelize.define('User', {
    name: DataTypes.STRING,
    birth: DataTypes.DATE,
    country_code: DataTypes.INTEGER
  }, {
    classMethods: {
      associate: function(models) {
        // associations can be defined here
      }
    }
  });
  return User;
};
```

### migrationの実行

`db:migrate` コマンドで生成したスクリプトをDBに適用します。
`--env` オプションを指定するとその環境のみに適用されます。
指定しない場合は `config.json` に記述されたすべての環境に適用されるので普通は `--env` を指定しますね。

```bash
node_modules/.bin/sequelize db:migrate --env development
```

pgAdminで確認してみると `users` テーブルと `SequelizeMeta` テーブルが作成されているのが分かります。 `SequelizeMeta` テーブルにはmigrationファイルのファイル名のレコードが作成されます。

![pgAdmin.png](https://qiita-image-store.s3.amazonaws.com/0/106217/14092494-df44-4565-4c7c-50ccddbb0e9b.png)

ロールバックする場合は以下のコマンドを実行します。
ロールバックで指定した処理が実行され、 `SequelizeMeta` の該当のレコードが削除されます。

```bash
node_modules/.bin/sequelize db:migrate:undo --env development
```

## 既存テーブルへのカラム追加

### 雛型ファイルの生成

以下のコマンドで既存テーブルのマイグレーションスクリプトの雛型を生成できます。
`migrations` フォルダ下に `(実行日時)-user.js` というファイル名のJavaScriptファイルが作成されます。 `--name` オプションを指定しないと `(実行日時)-unnamed-migration.js` というファイル名です。
`migration:generate` も `migration:create` の結果はどちらも同じなので使いやすい方を利用しましょう。個人的には `migration:generate` の方がしっくりきます。

```bash
node_modules/.bin/sequelize migration:generate --name user
```

```bash
node_modules/.bin/sequelize migration:create --name user
```

```js
'use strict';

module.exports = {
  up: function (queryInterface, Sequelize) {
    // コメント部分は省略しています
  },

  down: function (queryInterface, Sequelize) {
    // コメント部分は省略しています
  }
};
```

以前に作成した `users` テーブルに `email` と `prefecture_code` カラムを追加します。
`queryInterface.addColumn` メソッドにテーブル名、カラム名とカラムの詳細設定を渡します。
残念ながらPostgreSQLは任意のカラムの後にカラムを追加することはできず、最後尾に追加することしかできません。そのため `before` `after` オプションがあるのですが機能しません。MySQLを利用する場合に検討してください。
`down` にはロールバック用のカラム削除処理を記述します。

```js
'use strict';

module.exports = {
  up: function (queryInterface, Sequelize) {
    return [
      queryInterface.addColumn('users', 'email', {
        type: Sequelize.STRING,
        after: 'country_code' // PostgreSQLでは無効なオプションです
      }),
      queryInterface.addColumn('users', 'prefecture_code', {
        type: Sequelize.INTEGER,
        allowNull: false,
        defaultValue: 0,
        after: 'email' // PostgreSQLでは無効なオプションです
      })
    ];
  },

  down: function (queryInterface, Sequelize) {
    return [
      queryInterface.removeColumn('users', 'email'),
      queryInterface.removeColumn('users', 'prefecture_code')
    ];
  }
};
```

### migrationの実行
テーブル作成時と同じく、 `sequelize db:migrate` で適用します。

```bash
node_modules/.bin/sequelize db:migrate --env development
```

ロールバック処理も同様です。

```bash
node_modules/.bin/sequelize db:migrate:undo --env development
```

カラム追加のマイグレーション実行後の `users` テーブルの構造です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/106217/6ebce3c5-2e86-7c47-ea30-1574501aa2f6.png)

<a name="add_index"></a>
## インデックスの追加

既存のテーブルにインデックスを追加する方法です。カラム追加の時と同じようにまずは雛型のファイルを作成します。

```bash
node_modules/.bin/sequelize migration:generate --name user
```

既存の `name` 列のインデックスとユニーク制約を追加、 新規に `last_login_at` 列を追加してインデックスにします。
インデックスの追加は `addIndex` メソッドを使用します。テーブル名と対象となる列の組み合わせ、インデックスの詳細を設定します。

```js
'use strict';

module.exports = {
  up: function (queryInterface, Sequelize) {
    return [
      queryInterface.addIndex(
        'users',
        ['name'],
        {
          indexName: 'users_name_index',
          indicesType: 'UNIQUE' // ユニークインデックスになります
        }
      ),
      queryInterface.addColumn('users', 'last_login_at', {
        type: Sequelize.DATE
      }),
      queryInterface.addIndex(
        'users',
        ['last_login_at'],
        {
          indexName: 'users_last_login_at_index' // こちらは単なるインデックス
        }
      ),
    ];
  },

  down: function (queryInterface, Sequelize) {
    return [
      queryInterface.removeIndex('users', 'users_name_index'),
      queryInterface.removeIndex('users', 'users_last_login_at_index')
    ];
  }
};
```

`sequelize db:migrate` でマイグレーションを実行すると2つのインデックスが作成されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/106217/3d044259-e5cc-7379-2003-fbd72e98e778.png)

<a name="composite_primary_key"></a>
## 複合主キーの記述方法

複数の列で一意になる複合主キーが必要な場合は主キーを構成する列全てに `primaryKey: true` を記述します。以下のテーブルは `user_id` と `friend_id` の2つで複合主キーとなります。

```js
'use strict';
module.exports = {
  up: function(queryInterface, Sequelize) {
    return queryInterface.createTable('friends', {
      user_id: {
        type: Sequelize.INTEGER,
        allowNull: false,
        primaryKey: true
      },
      friend_id: {
        type: Sequelize.INTEGER,
        allowNull: false,
        primaryKey: true
      },
      created_at: {
        type: Sequelize.DATE,
        allowNull: false
      },
      updated_at: {
        type: Sequelize.DATE,
        allowNull: false
      }
    });
  },
  down: function(queryInterface, Sequelize) {
    return queryInterface.dropTable('friends');
  }
};
```

マイグレーションを実行すると複合主キーを持つ `friends` テーブルが作成されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/106217/4e287e14-ffa9-f983-28f5-412fa55e069e.png)

<a name="command_list"></a>
## コマンドまとめ

ここで紹介したコマンドの一覧です。

|内容|コマンド|
|---|----|
|モデル＋雛型作成|sequelize model:create --name [モデル名] --underscored --attributes [カラム名:データ型]|
|雛型作成|sequelize migration:generate --name [ファイルの接尾辞]|
|マイグレーション実行|sequelize db:migrate --env [対象の環境]|
|巻き戻し|sequelize db:migrate:undo --env [対象の環境]|

## DB上級者向けのTIPS

誤ったマイグレーション操作のために `sequelize db:migrate:undo` ではどうにもならくなった場合は `SequelizeMeta` のレコードを直接削除する、追加するなどで適用履歴を操作することが可能です。
あくまで `SequelizeMeta` の履歴管理の仕組を把握している、かつSQLに長けた上級者向けの技です。