[Node.js] SequelizeでMySQLを利用する - その4「マイグレーション編」

Node.jsの代表的なO/RMであるSequelizeの第四弾。

第1回ではインストールから基本的な利用方法、第2回ではSELECT文の使い方、第3回ではトランザクションを取り上げました。

今回は「マイグレーション」のお話です。 Sequelizeから提供されているCLIツールを利用すると、モデルの内容をコマンド一発でMySQLへ反映することができます。最初の1回目だけではなく運用開始後に差分を反映することもできます。他のフレームワークなどでも見かけますよね。Ruby on Railsで初めてこの手のツールを触ったときは感動したものですw

インストール

チュートリアル

事前準備や基本的な利用方法については第1回を参照ください。 blog.katsubemakito.net

CLIツール

package.jsonのあるディレクトリへ移動し追加でCLIツールをインストールします。-Dオプションは--save-devと同じ意味です。インストールが正常に完了するとnode_modules/.binの下にsequelize-cliコマンドが入ります。

$ npm install -D sequelize-cli

実際に動作するか確認します。今回は6.2.0が入りました。

$ npx sequelize-cli --version
Sequelize CLI [Node: 14.17.0, CLI: 6.2.0, ORM: 6.6.5]
6.2.0

CLIの基本的な利用方法

テンプレを生成

一番最初にテンプレを自動生成してくれるコマンドを実行します。

$ npx sequelize-cli init

これで以下の4つのディレクトリとその中にいくつかのファイルが生成されます。

config
データベースへの接続方法を記録した設定ファイルの保管場所。中にいる「config.json」を編集します。
models
プロジェクトのすべてのモデルの保管場所。
migrations
マイグレーション実行時に利用するファイル置き場。
seeders
テーブルの新規作成後に初期データとして挿入するファイル置き場。シードファイル。

config/config.jsonを編集

MySQLへの接続情報を記入します。3つの環境が書かれていますが、これは自由に増減できます。ちなみにデフォルトではdevelopmentが採用されその他の環境に接続する場合は環境変数などで切り替えます。

{
  "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"
  }
}

なおこのファイルはJavaScriptではなく「JSON」です。JavaScriptのようにコメントを書いたり文字列をシングルコーテーション(')で囲うことはできません。同様に配列やオブジェクトの一番最後の要素の後ろに余計なカンマも許されません。文法のチェックが非常に厳しいのでご注意を。

最初のモデルを作成

model:generateオプションを付けるとモデルのテンプレを自動生成してくれます。以下ではnameとemailのカラムを持ったUserモデルを作成します(改行していますが1行で書いてももちろんOKです)

$ npx sequelize-cli model:generate \
    --name User \
    --attributes name:string,email:string

以下の2つのファイルが生成されます。

models/user.js
モデルのテンプレです。基本的にここに処理を加筆していきます。
migrations/20210809142244-create-user.js
マイグレーション用のファイルです。MySQLへ反映する内容が書かれています。こちらもテーブル定義を変更する場合に必要に応じて編集します。

マイグレーション

先ほど生成されたマイグレーション用ファイルを元にモデルをMySQLへ反映します。

$ npx sequelize-cli db:migrate

実際に確認してみる

MySQLを覗くと2つのテーブルが作成されています。「SequelizeMeta」はマイグレーション管理用、「Users」が先ほど作成したモデルを反映したテーブルです。

mysql> show tables;
+--------------------------------+
| Tables_in_database_development |
+--------------------------------+
| SequelizeMeta                  |
| Users                          |
+--------------------------------+

Userモデルのテーブル構成も意図した通りの物になっていますね。

mysql> desc Users;
+-----------+--------------+------+-----+---------+----------------+
| Field     | Type         | Null | Key | Default | Extra          |
+-----------+--------------+------+-----+---------+----------------+
| id        | int(11)      | NO   | PRI | NULL    | auto_increment |
| name      | varchar(255) | YES  |     | NULL    |                |
| email     | varchar(255) | YES  |     | NULL    |                |
| createdAt | datetime     | NO   |     | NULL    |                |
| updatedAt | datetime     | NO   |     | NULL    |                |
+-----------+--------------+------+-----+---------+----------------+

ちなみに「SequelizeMeta」にはマイグレーション時に利用したファイルが記録されます。これは単なる履歴ではなくマイグレーションの処理を行う際に利用されますので間違えて消さないよう気をつける必要があります。

mysql> select * from SequelizeMeta;
+-------------------------------+
| name                          |
+-------------------------------+
| 20210809142244-create-user.js |
+-------------------------------+

元に戻すには?

先ほど実行したマイグレーションを無かったことにできます。

$ npx sequelize-cli db:migrate:undo

MySQLを覗くと先ほど作成したUsersテーブルが消えて無くなり、「SequelizeMeta」から先ほど実行されたファイル名も消えました。

mysql> show tables;
+--------------------------------+
| Tables_in_database_development |
+--------------------------------+
| SequelizeMeta                  |
+--------------------------------+

mysql> select * from SequelizeMeta;
Empty set (0.00 sec)

別の環境にマイグレーションするには?

--envオプションで環境を指定します。以下ではtest環境を指定しています。

$ npx sequelize-cli db:migrate --env test

環境変数NODE_ENVに環境名をセットしてから実行する方法もありますが、うっかり設定したのを忘れて見当違いの環境を触る可能性があるので、恒常的に変更する場合に.bashrc/.zshrcなどに書いて利用するのがオススメです。

$ export NODE_ENV=test 

モデルの利用方法

呼び出し方

作成したUserモデルの利用方法は至って簡単。 user = require('./models/user')としても良いのですが、「models/index.js」をrequireした上で実際に利用したいモデル名をメソッドチェーンで書くと自動的に呼び出してくれます。一度にたくさんのモデルを使いたい場合もこれならシンプルに書けますね。

const models = require('./models')

!(async ()=>{
  await models.User.create({
    name: 'Kawasaki',
    email: 'kawasaki@example.com'
  })

  const rows = await models.User.findAll()
  rows.forEach(row => {
    console.log(`${row.name} ${row.email}`)
  })
})()

環境を変更する

デフォルトではconfog.jsonで指定した「development」環境を見ているわけですが、CLIツールと同様に環境変数NODE_ENVに環境名をセットすることで変更することができます。

$ export NODE_ENV=test 
$ node foo.js

この処理は「models/index.js」の7行目に書かれていますので、必要に応じて書き換えることもできます。実際に利用する際にはdotenvを使った方が楽ちんですね。

const env = process.env.NODE_ENV || 'development';

初期データ

先ほど作成したUserテーブルに初期データを入れてみます。Sequelizeではseedと呼びます。

seedファイルを作成

まずはどういったデータを入れるのか定義するためのテンプレをコマンドで生成します。

$ npx sequelize-cli seed:generate --name demo-user

するとseedersディレクトリの下に以下のようなファイルが作成されます。

'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    // ここに初期データを定義
  },

  down: async (queryInterface, Sequelize) => {
    // undoする際の処理を定義
  }
};

では先ほどのファイルに実際のデータを記述します。注意点としてはテーブル名は複数形の方を書く必要があるのと、タイムスタンプ機能を利用している場合はcreatedAtやupdatedAtなどもデータを定義する必要があります。

'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    return queryInterface.bulkInsert('Users', [
      {name: 'Kawasaki', email: 'kawasaki@example.com', createdAt: new Date(), updatedAt: new Date()},
      {name: 'Honda',    email: 'honda@example.com',    createdAt: new Date(), updatedAt: new Date()},
      {name: 'Yamaha',   email: 'yamaha@example.com',   createdAt: new Date(), updatedAt: new Date()},
      {name: 'Suzuki',   email: 'suzuki@example.com',   createdAt: new Date(), updatedAt: new Date()}
    ]);
  },

  down: async (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('Users', null, {});
  }
};

MySQLへ初期データを反映

コマンド一発です。

$ npx sequelize-cli db:seed:all

実際にMySQL上のテーブルを確認すると無事にINSERTされていました。

mysql> select * from Users;
+----+----------+----------------------+---------------------+---------------------+
| id | name     | email                | createdAt           | updatedAt           |
+----+----------+----------------------+---------------------+---------------------+
|  1 | Kawasaki | kawasaki@example.com | 2021-08-09 16:07:47 | 2021-08-09 16:07:47 |
|  2 | Honda    | honda@example.com    | 2021-08-09 16:07:47 | 2021-08-09 16:07:47 |
|  3 | Yamaha   | yamaha@example.com   | 2021-08-09 16:07:47 | 2021-08-09 16:07:47 |
|  4 | Suzuki   | suzuki@example.com   | 2021-08-09 16:07:47 | 2021-08-09 16:07:47 |
+----+----------+----------------------+---------------------+---------------------+

元に戻すには?

これもコマンド一発です。seedファイルのdownで定義した内容がそのまま実行されます。

$ npx sequelize-cli db:seed:undo

MySQL上のテーブルを確認すると実行前の空の状態に戻っていました。

mysql> select * from Users;
Empty set (0.00 sec)

追加でマイグレーションする

運用開始後にテーブルやカラムを追加したり削除したくなることがありますよね。そんな場合もマイグレーション機能で対応できます。

ここではサンプルとしてUsersテーブルにカラムを追加する作業を行います。

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

model:generateすると自動的にマイグレーション用のファイルも生成してくれましたが、このファイルだけを生成することもできます。

コマンド一発で生成できます。--nameオプションで指定する値は自由に付けることができます。

$ npx sequelize-cli migration:generate --name user-addcloumn

migrationsディレクトリの下に「20210810034653-user-addcloumn.js」という名前のファイルが作成されました。

編集する

以下のようなファイルが作成されるので、up内にマイグレーション時の処理を、down内にundoした際の処理を書いていきます。

生成直後のコード

'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    // ここにカラムを追加する処理
  },

  down: async (queryInterface, Sequelize) => {
    // ここにundoした際の処理
    // (つまりカラムを削除する)
  }
};

カラム追加に対応したコード

今回はUsersテーブルにaddressとageという名前のカラムを追加します。

'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    return Promise.all([
      // Users.addressを追加
      queryInterface.addColumn('Users', 'address', {
        type: Sequelize.STRING(128),
        after: 'email'                // emailの後ろに追加(MySQLのみ)
      }),
      // Users.ageを追加
      queryInterface.addColumn('Users', 'age', {
        type: Sequelize.INTEGER,
        after: 'name'                 // nameの後ろに追加(MySQLのみ)
      }),
    ])
  },

  down: async (queryInterface, Sequelize) => {
    // Users.address, Users.ageを削除
    return Promise.all([
      queryInterface.removeColumn('Users', 'address'),
      queryInterface.removeColumn('Users', 'age')
    ]);
  }
};

利用できるメソッド

QueryInterfaceで用意されているメソッドが利用可能です。

以下はその一例です。

# メソッド名 機能
1 createDatabase データベースを作成
2 dropDatabase データベースを削除
3 createTable テーブルを作成
4 dropTable テーブルを削除
5 renameTable テーブル名を変更
6 addColumn カラムを追加
7 changeColumn カラムの属性を変更
8 removeColumn カラムを削除
9 renameColumn カラム名を変更
10 addIndex インデックスを追加
11 removeIndex インデックスを削除

マイグレーションを実行

コマンドは全く同じです。SequelizeがSequelizeMetaテーブルを参照し実行していないマイグレーションファイル内のupで定義した処理を実行してくれます。

$ npx sequelize-cli db:migrate

MySQLを確認するとaddressとageが追加されているのが確認できました。

mysql> desc Users;
+-----------+--------------+------+-----+---------+----------------+
| Field     | Type         | Null | Key | Default | Extra          |
+-----------+--------------+------+-----+---------+----------------+
| id        | int(11)      | NO   | PRI | NULL    | auto_increment |
| name      | varchar(255) | YES  |     | NULL    |                |
| age       | int(11)      | YES  |     | NULL    |                |
| email     | varchar(255) | YES  |     | NULL    |                |
| address   | varchar(128) | YES  |     | NULL    |                |
| createdAt | datetime     | NO   |     | NULL    |                |
| updatedAt | datetime     | NO   |     | NULL    |                |
+-----------+--------------+------+-----+---------+----------------+

元に戻す

こちらのコマンドも同じくundoを実行します。最後に実行したマイグレーションのdown内の処理を実行してくれます。

$ npx sequelize-cli db:migrate:undo

テーブルからも削除されているのが分かりますね。

mysql> desc Users;
+-----------+--------------+------+-----+---------+----------------+
| Field     | Type         | Null | Key | Default | Extra          |
+-----------+--------------+------+-----+---------+----------------+
| id        | int(11)      | NO   | PRI | NULL    | auto_increment |
| name      | varchar(255) | YES  |     | NULL    |                |
| email     | varchar(255) | YES  |     | NULL    |                |
| createdAt | datetime     | NO   |     | NULL    |                |
| updatedAt | datetime     | NO   |     | NULL    |                |
+-----------+--------------+------+-----+---------+----------------+

続き

blog.katsubemakito.net

参考ページ