プロローグ
Sequelizeは、公式によるとPromiseベースのNode.jsのORMツールとのこと。
ORMについてはちゃんとは分かってないけど、「コード上でのデータベース操作を簡単にしてくれるツール」的なモノらしい。これに関しては、調べるとインピーダンスミスマッチとかいう難しそうな概念がでてきたりとかして深みにはまりそうだったので、ワイは「なんか便利らしいしそれ使うんならORMを使わない方法と比べてどうとかはとりあえずいいや」ってことにしたんで今回はスルーするンゴ。
今回はSequelize-cliというCLIツールを使ってアプリケーションプロジェクトの環境を構築し、DB側のテーブルやデータを作成する手段から、Sequelizeを通したDBに対するCRUD操作までを2つの記事にわたって網羅的に解説していく。
開発環境
sequelize: 6.21.0sequelize-cli: 6.4.1node: 17.1.0npm: 8.1.2
準備
Sequelize-cliはSequelizeとは別のパッケージであるため、別個にインストールする必要がある。
npm i -D sequelize-cli
テーブルやデータの作成はsequelize-cliがなくても出来るけど、使ったほうが効率的に開発ができるので積極的に使っていきたい。まずは以下を実行してテーブルやデータの作成のための準備をする。
npx sequelize init
これによってプロジェクト直下に/config/config.json,/migrations/,/models/index.jsが作成される。この構成やconfig.jsonの名前を変更したい場合は、上記コマンド実行前にプロジェクト直下に.sequelizercを作成してそこで指定しておく。 ⇒詳細はこちら
そしてsequelizeとデータベースを連携させるため、作成されたconfig.jsonのdevelopmentの各種設定を自身の環境のものに変えておく。
テーブル・モデルの定義
これでテーブルやデータの作成の準備が出来たので、まずはマイグレーションファイルとモデル(それぞれ何かは後述)を作成する。今回は学籍番号、下の名前、上の名前、論理削除用フィールド(後述)を属性として持つ学生モデルを作る(今回作成するのは1モデルのため、モデル間の関連性の定義や、Sequelizeでのテーブル結合(即時読み込み)の仕方などについては扱わない)。こちらの記事にまとめてるンゴ。
npx sequelize model:generate --name Student --attributes studentNo:string,firstName:string,lastName:string,deletedAt:date
これによって/migrations/YYYYMMDDHHMMSS-create-student.js(YYYYMMDDHHMMSSの部分は実行時点の日付時刻)が、/models/student.jsが`それぞれ作成される。
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Students', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
studentNo: {
type: Sequelize.STRING
},
firstName: {
type: Sequelize.STRING
},
lastName: {
type: Sequelize.STRING
},
deletedAt: {
type: Sequelize.DATE
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('Students');
}
};
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Student extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// define association here
}
}
Student.init({
studentNo: DataTypes.STRING,
firstName: DataTypes.STRING,
lastName: DataTypes.STRING,
deletedAt: DataTypes.DATE
}, {
sequelize,
modelName: 'Student',
});
return Student;
};
(自動生成されたコードは、自分のエディタの設定とインデントの行数が違っている場合があるため、自動生成後すぐにAuto Indentなどでフォーマットを整えると良い)
ここでまずマイグレーションファイルを見てみると、createTable()やdropTable()というメソッドがあることが分かる。実はマイグレーションファイルはデータベースのテーブル定義とか定義変更のためのファイルになる。
ところでよく見るとテーブル名がStudentsと複数形になっていることに気づくと思う。これはSequelizeがinflectionというライブラリを使って自動で複数形に行っているみたいで、テーブル名を単数形にしたい場合は手動でマイグレーションファイルをいじる必要があるからちょっとめんどくさい…。しかもperson → peopleみたいな複数形が不規則的な名詞もちゃんと変換されるってなってるんだけど、試しにモデル名stimulus(単数形)で作ってみたらマイグレーションファイルのテーブルはstimuli(複数形)とはならずにstimulusのままで、ちょっとここらへんは分からない部分があったり。
で、テーブルを単数形にする場合はYYYYMMDDHHMMSS-create-student.jsのupの部分のテーブル名だけじゃなくてdownのほうも単数形にしとかないと、マイグレーションを実行して作成したStudentを、migrate:undoして状態を元に戻す(テーブルを削除する)際にmigrate:undoが上手くいかないので注意が必要。今回はモデルのほうに合わせてテーブル名も単数形にする。
それから同じくマイグレーションファイルを見ると、id, createdAt, updatedAtのフィールドが追加されていて、idはプロパティにあるように主キーとして設定されている。ワイとしてはmodel:generateのコマンドの時に属性として指定したstudentNoを主キーにしたかったので、最終的に以下のようにマイグレーションファイルを変更する。
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Student', {
studentNo: {
allowNull: false,
primaryKey: true,
type: Sequelize.STRING
},
firstName: {
allowNull: false,
type: Sequelize.STRING
},
lastName: {
allowNull: false,
type: Sequelize.STRING
},
deletedAt: {
type: Sequelize.DATE
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('Student');
}
};
さらに、マイグレーションファイルの変更に合わせてモデルのほうを以下のように変更する。
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Student extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// define association here
}
}
Student.init({
studentNo: {
type: DataTypes.STRING,
primaryKey: true
},
firstName: {
type: DataTypes.STRING,
allowNull: false
},
lastName: {
type: DataTypes.STRING,
allowNull: false
}
}, {
sequelize,
modelName: 'Student',
freezeTableName: true,
paranoid: true
});
return Student;
};
ポイントとしては、
-
studentNoに主キーとしての情報を追加した。 - モデル単独の変更として、上記
Student.init()の第二引数のオブジェクトにプロパティを追加した。まずfreezeTableName: true。これがないと、Sequelizeを使ったCRUD操作の際に、テーブル名を複数形に自動変換してSQL文を実行することになるため、モデル名とテーブル名を単数形で統一している場合、テーブル名が見つからずにエラーになる。次にparanoid: true。これがないと論理削除機能が使えないため、ここで追加しておく。 -
Student.init()の第一引数ではcreatedAtとupdatedAtをあえて追加せず、逆にdeletedAtを削除している。まずcreatedAtとupdatedAt第二引数のオブジェクトでtimestampsをfalseにしていない限り暗黙的に同プロパティの値はtrueと解釈され、第一引数に上記2つのフィールドを指定していなくてもDB側のテーブルにカラムとして存在してさえいれば、Sequelizeでレコードを取得してきた際の結果に同フィールドが含まれるようになる(attributeで結果を射影している場合は別)。さらにparanoid: trueを追加したことで、deletedAtについてもDB側にカラムとして存在さえしていれば、モデルファイルに定義がなくてもレコード取得の際に同フィールドを含んで結果を返してくれる。
ここで、なぜ今回自動生成されたcreatedAtとupdatedAtを消さずに採用しているかについて。上記の通り、timestamps: falseを指定していない限りSequelizeはDBに上記2カラムがあるものとしてレコードを取得しようとするので、上記2つをマイグレーションファイルから消す場合、モデルにおいてtimestamps: falseを指定しなきゃいけないんだけど、そうすると今度は論理削除が機能しなくなるという致命的な問題が発生するため今回はカラムとして採用している。
ところでモデルとは何かの説明なしで中身だけ変更しちゃったのでここで補足。モデルはDBのテーブルをJS側で扱うためのもので、JS的にはModelクラスを拡張したクラスらしい。モデルの定義は今回はCLIが勝手にModel.init()としてやってくれてる(sequelize.define('Model', {})とする方法もあるけど、これも内部でModel.init()を実行しているからやってることは同じらしい)。
テーブルの作成
さて、これでテーブル(とモデル)の定義ができたんだけど、まだDBにはStudentテーブルができてないから、以下のコマンドでマイグレーションを実行する。
npx sequelize db:migrate
これを実行してもテーブルが作成されない場合は、config/config.jsonに記載したDB情報とかが間違ってないか確認って感じで。さて、上記コマンドが成功するとDBにStudentテーブルとともにSequelizeMetaというテーブルが作成される。現時点でのレコードは以下の通り。
| name | |
|---|---|
| 1 | YYYYMMDDHHMMSS-create-student.js |
実行したコマンドとレコードの中身からも分かるように、SequelizeMetaはマイグレーション実行の履歴のためのテーブル。npx sequelize db:migrateが実行されると、SequelizeMetaのレコードに存在しない = まだ実行されていないマイグレーションファイルが実行されるという仕組みになっている。
ここで以下を実行すると、DB上でStudentテーブルが削除され、SequelizeMetaテーブルでは先ほど見たレコードが消される。
npx sequelize db:migrate:undo
| name |
|---|
上記のnpx sequelize db:migrate:undoは一番最後に実行したマイグレーションをなかったことにして元の状態に戻すコマンドであるのに対して、
npx sequelize db:migrate:undo:all
とすると今までのすべてのマイグレーションをなかったことにして初期状態に戻すことが出来るが、今回は1回しかマイグレーションを実行していないため、挙動としては同じになる。また、
npx sequelize-cli db:migrate:undo:all --to XXXXXXXXXXXXXX-create-student.js
のようにマイグレーションファイルを指定することで、指定したマイグレーションファイルを実行する前の状態に戻すことが出来る(例えば3つのマイグレーションファイルを実行した後、2つ目のマイグレーションファイルを--toで指定してdb:migrate:undo:allした場合、2つ目と3つ目のマイグレーションファイルが実行される前の状態に戻る)。
データの挿入準備
さてこれでSequelize-cliを通して、モデルのフィールドをカラムとして持つテーブルをDBに作成するということが出来た。次はSequelize-cliを使ってDBにサンプルデータを挿入する方法(シーディングと呼ばれる)をみていく。まずは以下を実行し、シーディングのためのデータを記述するファイルを作成する。
npx sequelize seed:generate --name student
実行するとseeders/YYYYMMDDHHMMSS-student.jsが作成される。
'use strict';
module.exports = {
async up (queryInterface, Sequelize) {
/**
* Add seed commands here.
*
* Example:
* await queryInterface.bulkInsert('People', [{
* name: 'John Doe',
* isBetaMember: false
* }], {});
*/
},
async down (queryInterface, Sequelize) {
/**
* Add commands to revert seed here.
*
* Example:
* await queryInterface.bulkDelete('People', null, {});
*/
}
};
上記のシードファイルを編集して、DBに挿入するサンプルデータを準備していく。
'use strict';
const data = [{
studentNo: '001',
firstName: '翔平',
lastName: '大谷'
},{
studentNo: '002',
firstName: '有',
lastName: 'ダルビッシュ'
},{
studentNo: '003',
firstName: '雄星',
lastName: '菊池'
},{
studentNo: '004',
firstName: '健太',
lastName: '前田'
},{
studentNo: '005',
firstName: '拓一',
lastName: '沢村'
}];
module.exports = {
async up (queryInterface, Sequelize) {
data.forEach((each) => {
each.createdAt = new Date();
each.updatedAt = new Date();
});
await queryInterface.bulkInsert('Student', data, {});
},
async down (queryInterface, Sequelize) {
await queryInterface.bulkDelete('Student', null, {});
}
};
ポイントとしては、
- 挿入するデータは
queryInterface.bulkInsert()の第二引数にベタ書きしてもいいんだけど、データの量が多いと可読性が低下してしまうので、変数に定義してそれを代入する形にしている。 - 作成日時の
createdAtは、このシードファイルを実行した日時がカラムの値として格納されるように、ここでnew Date()を設定する。更新日時のupdatedAtは、レコードの値が更新されると自動でupdatedAtも更新されるから、データ挿入の際はほっといてもいいんだけど、マイグレーションファイルのほうでallowNull: falseを設定しちゃってるので、こちらもとりあえずnew Date()を設定しておく。ちなみにここらへんの情報が公式ドキュメントになく、CLIのissueのほうでひんしゅくを買っていたりもする(ここらへん)。というか自分もこの情報を参考に値の設定を行っている。(実はモデルのほうでcreatedAtとupdatedAtのプロパティとしてdefaultValue: DataTypes.NOWを設定しとけばシーディングの時に勝手に値を挿入してくれんじゃねって思って試したけどダメだった経緯がある…)
データの挿入
さて、シーディングの準備が出来たので、以下のコマンドを実行する。
npx sequelize db:seed:all
これでDBのStudentテーブルに、シードファイルで指定したデータが挿入される。
| studentNo | firstName | lastName | deletedAt | creadtedAt | updatedAt | |
|---|---|---|---|---|---|---|
| 1 | 001 | 翔平 | 大谷 | null | 2022-XX-XX XX:XX:XX.XXX+09 | 2022-XX-XX XX:XX:XX.XXX+09 |
| 2 | 002 | 有 | ダルビッシュ | null | 2022-XX-XX XX:XX:XX.XXX+09 | 2022-XX-XX XX:XX:XX.XXX+09 |
| 3 | 003 | 雄星 | 菊池 | null | 2022-XX-XX XX:XX:XX.XXX+09 | 2022-XX-XX XX:XX:XX.XXX+09 |
| 4 | 004 | 健太 | 前田 | null | 2022-XX-XX XX:XX:XX.XXX+09 | 2022-XX-XX XX:XX:XX.XXX+09 |
| 5 | 005 | 拓一 | 沢村 | null | 2022-XX-XX XX:XX:XX.XXX+09 | 2022-XX-XX XX:XX:XX.XXX+09 |
(createdAtとupdatedAtはマスキングしてるンゴ)
実行したシードデータをDBから削除したい場合は、以下の3つのコマンドが使える。
1.一番最後に実行したシードデータを削除する
npx sequelize db:seed:undo
2.指定したシードデータを削除する
npx sequelize db:seed:undo --seed YYYYMMDDHHMMSS-student.js
3./seeders/以下のすべてのシードデータを削除する
npx sequelize db:seed:undo:all
これで、Sequelize-cliを使ってDB上にテーブルを作成し、そこにレコード(シードデータ)を挿入することが出来た。キリがいいので今回はここまで。今回の続きはこちら