Node.jsでO/RMと言えば「Sequelize」です。
PostgreSQL, MySQL, MariaDB, SQLite, Microsoft SQL Serverなど主要なRDBに対応しており、AWS上での利用報告もありますのでRDSなどでももちろん使えます。また現在たどれる最古のバージョンv1.1.2は2011年にリリースされていることからすでに10年以上の歴史がありもちろん現在もメンテナンスが継続しています。これは安心して利用できますね!
というわけで今回はSequelizeを初めて触るチュートリアルとしてまとめていきます。現行バージョンであるv6のドキュメントを元にしています。
インストール
MySQL
詳細は割愛しますがDockerで用意しても良いですし、Amazon LinuxやCentOSの場合は以下の記事のMySQLの項目をご覧ください。 blog.katsubemakito.net
macOSの場合はHomeBrewで入れることもできます。 blog.katsubemakito.net
MySQLのインストールが終わったら練習用に適当なユーザーとデータベース(Scheme)を作成しておきます。テーブルは作らなくてOKです(というか作らないでください)
Node.js
Node.jsがインストールされている環境に適当なディレクトリを作成、npm init
を実行しpackage.jsonを生成します。
$ mkdir test; cd test; $ npm init
sequelizeとmysql2を入れれば準備完了です。
$ npm install sequelize mysql2
無印のmysqlは2年ほど更新されていないようなので、Sequelizeに限らず現在では後継であるmysql2を利用した方が良いようです。
基本的な使い方
接続からシンプルなCRUDするまでを取り上げます。
サンプルコードは必要な部分だけ抜粋していますので、Top-Level Awaitに未対応の環境でawait
が最初に付いている構文を実行する場合は、async
が付いている関数内かthen()
内で実行してください。
MySQLへ接続する
Sequelizeのインスタンスを生成する際に接続情報を指定します。
const { Sequelize, DataTypes } = require('sequelize') const sequelize = new Sequelize('database_name', 'user', 'password', { host: 'localhost', dialect: 'mysql', // logging: false })
- パスワードを設定していない場合は
null
を指定します。 dialect
はここではmysql
固定です。- 通常Sequelize経由で実行されたSQLが
console.log()
でデバッグ的に表示されますが、不要な場合はlogging: false
で非表示にできます。
複数のデータベースに同時に接続するような場合は、インスタンスを必要な数だけ作成します。またレプリケーションにも対応していますので、更新関係はマスター、SELECTはスレーブに発行すると言ったことも可能です。
モデルを準備
定義
データベースで言うテーブルの構成をNode.js上でも定義します。以下ではUserテーブルにnameとageというカラムを用意しています。
const User = sequelize.define('User', { // Userテーブル name: { type: DataTypes.STRING, // 文字列型 allowNull: false // Not Null }, age: { type: DataTypes.INTEGER // 整数型 } })
データ型
モデルの定義時に利用できるデータ型には一般的な物が各種用意されています。
Sequelize | MySQL |
---|---|
DataTypes.STRING | VARCHAR(255) |
DataTypes.TEXT | TEXT |
DataTypes.BOOLEAN | TINYINT(1) |
DataTypes.INTEGER | INTEGER |
DataTypes.BIGINT | BIGINT |
DataTypes.DOUBLE | DOUBLE |
DataTypes.FLOAT | FLOAT |
DataTypes.DECIMAL | DECIMAL |
DataTypes.DATE | DATETIME |
DataTypes.DATEONLY | DATE |
- 桁数の指定が可能です。
- 例えば
DataTypes.STRING(128)
とすると、SQLではVARCHARA(128)
となります。 - BIGINT, FLOAT, DOUBLE, DECIMALも同様です
- 例えば
- 整数型にUNSIGNED指定をする場合は
DataTypes.INTEGER.UNSIGNED
- 整数型にゼロ字詰めを指定する場合は
DataTypes.INTEGER.ZEROFILL
- UNSIGNEDとゼロ字詰めを同時に指定する場合は
DataTypes.INTEGER.UNSIGNED.ZEROFILL
変わったところだと以下のようにUUIDを自動生成してくれる機能もあります。MySQL上にはCHAR(36)
としてカラムが作成され、INSERT時にUUIDが自動的に挿入されます。
uniq_id: { type: DataTypes.UUID, defaultValue: Sequelize.UUIDV4 // もしくは Sequelize.UUIDV1 }
カラムのオプション
CREATE TABLE文で利用する様々な指定が用意されています。
key: { type: DataTypes.INTEGER, primaryKey: true, // PRIMARY KEY autoIncrement: true, // AUTO_INCREMENT comment: '主キー' // COMMENT }, data1: { type: DataTypes.STRING(32), unique: true, // UNIQUE }, flag1: { type: DataTypes.BOOLEAN, allowNull: false, // NOT NULL defaultValue: true // DEFAULT } bar_id:{ type: DataTypes.INTEGER, // 外部キー // 実際生成されるSQLは FOREIGN KEY (`bar_id`) REFERENCES `Bars` (`id`) references: { model: Bar, key: 'id' } }
- インデックスは別の場所で定義します。
テーブル全体のオプション
sequelize.define()
の第3引数にオプションを渡すことでテーブル全体の定義ができます。
const User = sequelize.define('User', { //------------------------- // ここにカラムの定義 //------------------------- name: { type: DataTypes.STRING, allowNull: false } }, { //------------------------- // ここにテーブル全体の定義 //------------------------- // MySQL上のテーブル名を複数形にしない(デフォルトはfalse) freezeTableName: true, // タイムスタンプ機能を利用する(デフォルトはtrue) // falseにするとcreatedAt, updatedAtのカラムが作成されません timestamps: false, // タイムスタンプ機能を一部調整(デフォルトはtrue) createdAt: false, // レコード生成時の時間を記録 updatedAt: false, // レコード更新時の時間を記録 // Booleanではなく文字列を指定するとMySQL上のカラム名を変更できます // createdAt: 'created_at', // updatedAt: 'updated_at' })
MySQLへ反映する
モデルの定義が終わったら、Model.sync()
を実行することでMySQL上に先ほどの定義を反映することができます。
await User.sync({force: true})
実際に以下のSQLが発行されます。
- まずテーブル名が「User」ではなく「Users」と複数形になります。気持ち悪い場合はオプションでモデル名とテーブル名を同じスペルにすることも可能です。
id
は自分でプライマリーキーを指定しなかった場合に自動的に付与されます(自分でプライマリーキーを指定した場合は付与されません)。createdAt
はupdatedAt
はレコードが生成、更新された日時を記録するものでこちらも勝手に付いてきます。他のフレームワークでもよく見かけますね。
CREATE TABLE IF NOT EXISTS `Users` ( `id` INTEGER NOT NULL auto_increment, `name` VARCHAR(255) NOT NULL, `age` INTEGER, `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB;
なおModel.sync()
の実行方法は次の3種類があります。特にforce: true
は既存のデータがすべて削除されてしまうため十分に注意する必要があります。開発中は便利なんですけどね。
- Model.sync()
- テーブルが存在しない場合は作成されます。すでに存在する場合は何もしません。
- Model.sync({ force: true })
- テーブルが存在する場合は削除した上で、新たに作成されます。
- Model.sync({ alter: true })
- テーブルの状態を事前にチェックし、カラム数やデータ型に変更がある場合は、その差分が適用されます。
開発中は非常に便利なんですが、本番で毎回この処理が入るのはパフォーマンス的に問題が出そうなので、Sequelize CLIなどで必要なときにマイグレーションする方が良さそうですね。
データ操作
INSERT
Model.create()
でINSERT文が発行できます。
const user = await User.create({name:'Honda', age:12})
実際に発行されるSQLは以下の通り。プライマリーキーであるidとレコードの作成/更新日時が入るcreatedAt, updatedAtも自動的に付与されているのがわかります。またVALUESの各項目がプレースホルダになっており自動的にエスケープされることが期待できますのでSQLインジェクションの心配も無さそうですね。
INSERT INTO `Users` (`id`,`name`,`age`,`createdAt`,`updatedAt`) VALUES (DEFAULT,?,?,?,?);
ちなみにModel.bulkCreate()
を使うと一度に複数のデータを挿入できます。裏側では1回のINSERTで複数のデータが挿入されています。
const user = await User.bulkCreate([ {name:'Honda', age:18}, {name:'Yamaha', age:16}, {name:'Suzuki', age:20}, {name:'Kawasaki', age:24} ])
SELECT
SELECT文を発行する方法はいくつかあります。検索結果の全レコードを取得するにはModel.findAll()
を使います。SELECT文で利用できるWHERE句やGROUP BY、ORDER BYなども使えます。もちろんoffsetやlimitなどで件数制限も可能。
const rows = await User.findAll() rows.forEach(row => { const id = row.id const name = row.name const age = row.age console.log(`${id}: ${name} ${age}`) })
他にも以下のようなメソッドが用意されています。
- Model.findByPk()
- プライマリーキーを引数として渡すと該当するレコードが、もし存在しない場合はnullが返されます。
- Model.findOne()
- 一番最初のレコードを1件だけ返します。
- Model.findOrCreate()
- 指定条件で検索し、該当するレコードがあれば返却、もし存在しない場合は指定したデータを挿入します。
- Model.findAndCountAll()
- 条件に一致したレコード数と、モデル内の全レコード数を同時に返却します。ページングするときとか便利ですね。
UPDATE
Model.update()
を使います。以下のコードでwhere
の条件に合致するレコードのage
を21に更新しています。
await User.update({ age: 21 }, { where: { name: 'Suzuki' } })
DELETE
Model.destroy()
を利用します。以下のコードでwhere
の条件に合致するレコードをすべて削除します。これdeleteとかremoveじゃないんですね。destroyとか書くのちょっと怖いw
await User.destroy({ where: { name: 'Yamaha' } })
データを丸ごと消してリセットしたい場合は同じくModel.destroy()
でTRUNCATEを実行可能です。
await User.destroy({ truncate: true })
MySQLから切断する
MySQLから切断するにはsequelize.close()
を呼ぶだけです。
await sequelize.close()
ここまでのサンプルコード
/** * Sequelize CRUD Sample * */ const { Sequelize, DataTypes } = require('sequelize') const sequelize = new Sequelize('database_development', 'root', null, { host: 'localhost', dialect: 'mysql', logging: false, }) //-------------------------------------------- // Models //-------------------------------------------- const User = sequelize.define('User', { name: { type: DataTypes.STRING(128), allowNull: false }, age: { type: DataTypes.INTEGER } }) //-------------------------------------------- // CRUD //-------------------------------------------- !(async()=>{ // MySQL上にテーブルを作成 await User.sync({alter: true}) // 既存のデータを削除(TRUNCATE) await User.destroy({ truncate: true }) // Userテーブルへデータを挿入 const user = await User.bulkCreate([ {name:'Honda', age:18}, {name:'Yamaha', age:16}, {name:'Suzuki', age:20}, {name:'Kawasaki', age:24} ]) // 'Suzuki'のageを21に更新 await User.update({ age: 21 }, { where: { name: 'Suzuki' } }) // 'Yamaha'を削除 await User.destroy({ where: { name: 'Yamaha' } }) // Userテーブルの全レコードを取得 const rows = await User.findAll(); rows.forEach(row => { const id = row.id const name = row.name const age = row.age console.log(`${id}: ${name} ${age}`) }) // MySQLから切断 await sequelize.close() })()