こんにちは!データベースの構築・管理の仕事をしていた元インフラエンジニアの管理人です。
Node.jsでデータベースMySQL, MariaDB, PostgresSQL, SQLiteなどと連携させたいときに便利なライブラリがSequelizeです。
この記事は Node.js + Express + MySQL の環境でSequelizeを導入する際の手順メモです。
SQLiteなど、他のDBを使う場合でもほとんど同じ流れになるので、参考にして頂けるかと思います。
導入からはじめ、実際にレコードをHTTPレスポンスで返すところまでを解説しています。
パッケージインストール
Express generatorでサーバの立ち上げができる状態になっていることを前提として解説していきます。
まずは sequelize を動かすのに必要なパッケージインストールを行います。
$ npm install -s nodemon sequelize-cli mysql2 sequelize
sequelize-cli はmigrationファイルを自動生成するコマンドを実行するなどに使います。
使用するディレクトリ
Expressを使っているので以下のようなディレクトリ構成になっているはず。
このうち、config、migrations、models、seedersは後述のコマンドで自動生成するので、まだ作らなくてOK。
Sequelizeを使う準備
Sequelizeを使うにあたって、まずは以下のコマンドを実行。
$ sequeliza init
そうすると、configディレクトリの下に config.json(環境設定ファイル)、modelsの下に index.js(モデル読込)が作られます。
自動生成された index.js は次のようなファイル。
'use strict'; const fs = require('fs'); const path = require('path'); const Sequelize = require('sequelize'); const basename = path.basename(__filename); const env = process.env.NODE_ENV || 'development'; const config = require(__dirname + '/../config/config.json')[env]; const db = {}; let sequelize; if (config.use_env_variable) { sequelize = new Sequelize(process.env[config.use_env_variable], config); } else { sequelize = new Sequelize(config.database, config.username, config.password, config); } fs .readdirSync(__dirname) .filter(file => { return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); }) .forEach(file => { const model = sequelize['import'](path.join(__dirname, file)); db[model.name] = model; }); Object.keys(db).forEach(modelName => { if (db[modelName].associate) { db[modelName].associate(db); } }); db.sequelize = sequelize; db.Sequelize = Sequelize; module.exports = db;
SequelizeのようなORM(ORマッパー)を使うときは、モデルをたくさん定義しておくことになります。
しかし、それらを個別にimportしなくても、網羅的に読み込んでくれるのがこの index.js さんです。
あとはモデルを自分で定義すればDBの読み書きができる。
簡単ですね!!
モデルの定義とマイグレーション
そして肝心のモデル定義ファイルを作成します。
いちいちモデルファイルをゼロから作るのは面倒なので、コマンド実行することでモデル定義ファイルとマイグレーションファイルのテンプレートを自動生成しましょう。
nameなどのカラム(項目)を持つ Owner というテーブルを作るときのコマンド例↓
$ sequelize model:generate --name Owner --attributes unique:string,class_id:bigint,name:string,followers:integer
これを実行すると以下のようなモデル定義が models の下に作られる(自動生成)。
'use strict'; module.exports = (sequelize, DataTypes) => { const Owner = sequelize.define('Owner', { unique: DataTypes.STRING, name: DataTypes.STRING, class_id: DataTypes.BIGINT, followers: DataTypes.INTEGER }, {}); Owner.associate = function(models) { // associations can be defined here }; return Owner; };
要件によって、制約とかアソシエーションとか付けたいですよね。
下記のように、モデル定義を要件に合うように加工していきましょう。
'use strict'; module.exports = (sequelize, DataTypes) => { const Owner = sequelize.define('Owner', { unique: { field: "unique", type: DataTypes.STRING, allowNull: false }, classId: { field: "class_id", type: DataTypes.BIGINT, allowNull: false }, name: { field: "name", type: DataTypes.STRING, allowNull: false }, followers: { field: "followers", type: DataTypes.INTEGER, allowNull: true } }, { tableName: "owners" // テーブル名を直接指定 timestamps: true, updatedAt: "updated_at", createdAt: "created_at", }); Owner.associate = function(models) { // アソシエーションの設定 Owner.belongsTo(models.ClassMap, { foreignKey: 'class_id', }); }; return Owner; };
サンプルでは、Ownerテーブルの各カラムに制約などを付加した上で、Ownerのクラスを定義している ClassMap テーブルとのアソシエーションを設定しました。
タイムスタンプカラムもsequelizeだと勝手に createdAt という名前になってしまうため、created_atと手動で設定しなおしています(テーブル設計と要件に合わせてください)。
一方、sequelize model:generateコマンドを実行するとマイグレーションファイルも自動的に作られます(下記参照)。
まだテーブルが作られていない段階なら、このファイルをテーブル設計に合わせて加工してから、migrationを実行します。
マイグレーションファイルはテーブルの作成・更新を自動化してくれるスクリプトみたいなもの。
ただし、プログラム中で sequelize が参照するのはモデルファイルだけなので注意が必要です。
マイグレーションファイルは開発時のテーブル定義の更新管理にだけ使うと考えればよいでしょう。
'use strict'; module.exports = { up: (queryInterface, Sequelize) => { return queryInterface.createTable('Owners', { id: { allowNull: false, autoIncrement: true, primaryKey: true, type: Sequelize.INTEGER }, unique: { type: Sequelize.STRING }, class_id: { type: Sequelize.BIGINT }, name: { type: Sequelize.STRING }, followers: { type: Sequelize.INTEGER }, createdAt: { allowNull: false, type: Sequelize.DATE }, updatedAt: { allowNull: false, type: Sequelize.DATE } }); }, down: (queryInterface, Sequelize) => { return queryInterface.dropTable('Owners'); } };
ちなみに、既存テーブルのmigrationファイルを作る場合にはこのQiitaが参考になる。
既存テーブル(サンプルではownersテーブル)にカラムを追加した例がこれ↓。
Promiseを返す必要があることに注意です。
'use strict'; module.exports = { up: (queryInterface, Sequelize) => { return Promise.all([ queryInterface.addColumn('owners', 'class_id', { type: Sequelize.BIGINT, after: 'unique' // for MySQL only }), queryInterface.addColumn('owners', 'followers', { type: Sequelize.INTEGER, after: 'name' // for MySQL only }), ]); }, down: (queryInterface, Sequelize) => { return Promise.all([ queryInterface.removeColumn('owners', 'class_id'), queryInterface.removeColumn('owners', 'followers') ]); } };
migrationの実行は次のコマンド。
環境指定を忘れずにしよう。
$ sequelize db:migrate --env development
DB接続と接続確認
さきほど sequelize init コマンドでconfigファイルが生成されていると思うので、そこに接続情報を更新しておきます。
試しに使うだけなので環境はdevelopmentに更新。
本番環境ではパスワード等の直書きは避けたい。環境変数を使おう。
{ "development": { "username": "root", "password": "passw0rd", "database": "sequelize", "config": { "host": "db", "dialect": "mysql", "logging": "console.log" } }, "test": { "username": "root", "password": null, "database": "database_test", "host": "127.0.0.1", "dialect": "mysql", "operatorsAliases": false }, "production": { "username": "root", "password": null, "database": "database_production", "host": "127.0.0.1", "dialect": "mysql", "operatorsAliases": false } }
app.jsにDB接続用のコードを追加。
接続を確認するだけの記述です。
try { sequelize.authenticate(); console.log('Connection has been established successfully.'); } catch (error) { console.error('Unable to connect to the database:', error); }
Model synchronization
Sequelize側で定義したモデルと実際のDBの定義に違いがでることも考えられますよね?
そういった場合には当然エラーを出したり予期しない動作をすることも考えられます。
それを防ぐために、Sequelizeではmodel synchronizationという仕組みが用意されています。
Model synchronizationを実行することで、プログラム側で定義したテーブルがDBにない場合は新規でテーブルを作り、テーブルのカラムがモデルと相違しているときはカラムを変更してくれるなど、良きに計らってくれます。
app.js などに記述を追加しておきます↓
models.sequelize.sync().then(() => { console.log('Seems like the backend is running fine...'); }).catch((err) => { console.log(err, 'Something went wrong with the operation'); });
DBから取得
いよいよDBからselectしてきましょう。
下記コードを router.get() の中に書けば、ブラウザ上にSELECTした内容を json形式で表示してくれます。
ここでは、findAll()ですべてのレコードを取得しています。
const db = {}; cdb.Owner = models.Owner; models.sequelize.transaction(async t => { const b = await Owner.findAll(); if (b !== null) { return Promise.resolve(res.send(b)); } else { return Promise.resolve(res.sendStatus(404)); } });
ちなみに、sequealizeのトランザクション設計には2通りあります。
Unmanaged transactions と Managed transactionsです。
簡単にいえば、手動でコミット&ロールバックするか、自動でしてくれるかの違い。
ここではmanagedの方を使用しているため、明示的にロールバックの記述をしていません。
-> manual
サンプルはここまで。
APIの仕様に応じてrouter.post()とかrouter.delete()にINSERTやDELETEのコード追加することで、sequelizeを使ったREST APIを作ることができます。
いろいろ試してみると楽しいですよ。