TypescriptでSequelize してみる
はじめに
前回 Sequelize の一番基本的な使い方を紹介したが、typescript で記述可能ならば、以降 ts で書きたいなと思ったので、今回は typescript の環境を紹介したいと思う。
ts で記述するメリットは、型が使えることだけではなく、エディタからの候補表示である。property や method の打ち間違いも ts ならば防げるわけだ。
目標(課題)
公式の typescript 導入紹介では、一つのファイルに Model 定義・初期化・associate・main ルーチンなどをすべて記述する。
実運用では、各 Model は models ディレクトリで管理するものだろう(sequelize-cli が初期化時に models ディレクトリを作るぐらいだ)。
なので、ただ ts 環境を作成するだけではなく、models ディレクトリで Model を管理し、main ルーチンは、main ファイルに記述する環境を構築したいと思う。
ディレクトリ構成
はじめてに最終的なディレクトリ構成や実行イメージを紹介する。
. ├── config.json # db 接続情報 ├── package.json ├── src │ ├── main.ts # mainルーチン │ └── models # Model管理 │ ├── index.ts │ ├── project.ts # projectモデル │ └── task.ts # taskモデル └── tsconfig.json
起動方法は、ts-node を利用してnpx ts-node src/main.ts
で可能とする。
インストール
Sequelize
npm -i sequelize mysql2
Typescript
npm -i -D typescript @types/node @types/validator @types/bluebird ts-node
npx typescript --init
で tsconfig.json を作成して、好きな設定をしてください。
src ディレクトリに main.ts ファイルを作成して、npx ts-node src/main.ts
がうまく動作することを確認できれば OK です。
model の定義
Model は、Project, Task を作成する。 それぞれの仕様は下図の通り。
両 Model とも id をプライマリキーにもち、title と description に加えて createdAt, updateAt をフィールドにもつ(createdAt, updateAt は sequelize で自動で作成されるので気にしなくてよい)。
さらに Project は複数の Task をもつ関係にあり、projectId が外部キーとなっている。
これで準備が整ったので実装に入ってみる。
src/models/project.ts
import { Sequelize, Model, DataTypes } from 'sequelize'; import { HasManyCreateAssociationMixin } from 'sequelize'; import Task from './task'; export default class Project extends Model { public id!: number; public title!: string; public description!: string; public readonly createdAt!: Date; public readonly updatedAt!: Date; // (3) 作成したprojectをprojectIdをもつtaskを作成するメソッド public createTask!: HasManyCreateAssociationMixin<Task>; // (1)初期化 public static initialize(sequelize: Sequelize) { this.init( { id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, primaryKey: true }, title: { type: DataTypes.STRING }, description: { type: DataTypes.TEXT } }, { sequelize, tableName: 'project' } ); return this; } // (2)テーブル関係を記述 public static associate() { this.hasMany(Task, { sourceKey: 'id', foreignKey: 'projectId', constraints: false }); } }
クラスプロパティ
sequelize の Model を継承する class を作成して export させる。クラスプロパティには、定義したフィールドを記述してやる。
しかしこの記述は typescript 用であり、sequelize が利用するには、次の initialize メソッドが必要になる。
static メソッド(initialize)
このメソッドは、引数に sequelize を受け取っているように、DB 接続設定完了した Sequelize インスタンスをもとに、実際に初期化作業を行っている。
type
プロパティやtableName
プロパティなど
前回記述した js の定義と若干異なるので注意が必要である。
static メソッド(associate)
このメソッドは、Project と Task Model の関係を sequelize に知らせるためのものである。this
は Project クラス自身を指して、第一引数にTask
を受け取る。
第二引数には、関係の詳細情報を与える(任意)。
- foreinKey
hasMany される側(Task)の外部キーを指定する 指定しない場合、ProjectId と大文字になる - sourceKey
source は、hasMany の主体(Project)側の外部キーとつながるキーを指定する。 なてくもよい。 - constraint
制約情報(外部キー)の有効化フラグ Project.sync({ force: true })を動作させるために false に設定。
createTask メソッド
HasManyCreateAssociationMixin
という型情報があってわかりづらいが、このメソッドは Project が作成された後、それに紐づく Task を作成できるものである。
なぜ static メソッドにするのか?
Sequelize の Model クラスは、save, update, remove メソッドの結果を受け取ったときインスタンスであるが、findAll などインスタンスを検索したり、作成するときはインスタンスではなくクラスの static メソッドで行っている。
1 レコード情報をもつのが、Model インスタンスであり、テーブル全体の情報をもつのが Model クラスなわけだ。
だから、createTask のように作成された project に紐づく task を作成するなばら、通常のメソッドになり、一方テーブル定義したり、テーブル同士の関係を記述するのは、static メソッドとなる。
同じように Task モデルも作成する
src/models/task.ts
import { Sequelize, Model, DataTypes, Association } from 'sequelize'; import Project from './project'; export default class Task extends Model { public id!: number; public title!: string; public description!: string; public deadline!: Date; public projectId!: number; public readonly createdAt!: Date; public readonly updatedAt!: Date; public static initialize(sequelize: Sequelize) { this.init( { id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, primaryKey: true }, title: { type: DataTypes.STRING }, description: { type: DataTypes.TEXT }, deadline: { type: DataTypes.DATE }, projectId: { type: DataTypes.INTEGER.UNSIGNED, allowNull: false } }, { sequelize, tableName: 'task' } ); return this; } public static associate() { this.belongsTo(Project, { foreignKey: 'projectId', constraints: false }); } }
モデルをまとめる
モデル定義が終わったので、次はこれら model をまとめる index.ts を作成しよう。
src/models/index.ts
import { Sequelize, Model } from 'sequelize'; import Project from './project'; import Task from './task'; const { database, username, password, host, dialect } = require('../../config'); // sequelizeインスタンス作成 const sequelize = new Sequelize(database, username, password, { host, dialect }); // (1)モデルを一つのオブジェクトにまとめる const db = { Task: Task.initialize(sequelize), Project: Project.initialize(sequelize) }; // (2)テーブル同士の関係を作成する Object.keys(db).forEach(tableName => { const model = db[tableName]; if (model.associate) { model.associate(); } }); export default db;
(1)複数のモデルをひとつのオブジェクトにまとめる
db
というオブジェクトに作成した Project, Task を一括管理させる。sequelize インスタンスを渡して、初期化された Project, Task モデルを管理する。
(2)テーブルの関係づけ
static メソッドの associate をもつならば、それを呼び出してテーブル同士を対応づけている。
main
ここまで来たらあとは簡単ですね。 typescript の恩恵を受けれるので打ち間違いもないだろうし。
src/main.ts
import models from './models'; (async () => { // Project, TaskテーブルをDrop & Create await models.Project.sync({ force: true }); await models.Task.sync({ force: true }); // projectインスタンス作成 const project = models.Project.build({ title: 'my awesome project', description: 'woot woot. this will make me a rich man' }); // projectのinsert const createdProject = await project.save(); // insertされたprojectにひもづくTaskを作成 await createdProject.createTask({ title: 'title', description: 'description' }); // projectのselect const projects = await models.Project.findAll({ include: [models.Task] }); console.log(projects.map(d => d.toJSON())); // taskのselect const tasks = await models.Task.findAll({ include: [models.Project] }); console.log(tasks.map(d => d.toJSON())); })();
npx ts-node src/main.ts
を実行すれば下記のように出力されるであろう。
Executing (default): SELECT `Project`.`id`, `Project`.`title`, `Project`.`description`, `Project`.`createdAt`, `Project`.`updatedAt`, `Tasks`.`id` AS `Tasks.id`, `Tasks`.`title` AS `Tasks.title`, `Tasks`.`description` AS `Tasks.description`, `Tasks`.`deadline` AS `Tasks.deadline`, `Tasks`.`projectId` AS `Tasks.projectId`, `Tasks`.`createdAt` AS `Tasks.createdAt`, `Tasks`.`updatedAt` AS `Tasks.updatedAt` FROM `project` AS `Project` LEFT OUTER JOIN `task` AS `Tasks` ON `Project`.`id` = `Tasks`.`projectId`; [ { id: 1, title: 'my awesome project', description: 'woot woot. this will make me a rich man', createdAt: 2019-07-21T04:23:17.000Z, updatedAt: 2019-07-21T04:23:17.000Z, Tasks: [ [Object] ] } ] Executing (default): SELECT `Task`.`id`, `Task`.`title`, `Task`.`description`, `Task`.`deadline`, `Task`.`projectId`, `Task`.`createdAt`, `Task`.`updatedAt`, `Project`.`id` AS `Project.id`, `Project`.`title` AS `Project.title`, `Project`.`description` AS `Project.description`, `Project`.`createdAt` AS `Project.createdAt`, `Project`.`updatedAt` AS `Project.updatedAt` FROM `task` AS `Task` LEFT OUTER JOIN `project` AS `Project` ON `Task`.`projectId` = `Project`.`id`; [ { id: 1, title: 'title', description: 'description', deadline: null, projectId: 1, createdAt: 2019-07-21T04:23:17.000Z, updatedAt: 2019-07-21T04:23:17.000Z, Project: { id: 1, title: 'my awesome project', description: 'woot woot. this will make me a rich man', createdAt: 2019-07-21T04:23:17.000Z, updatedAt: 2019-07-21T04:23:17.000Z } } ]
さいごに
ts 環境の構築と sequelize の説明両方が入ってしまったので、ややこしくなってしまった感いなめないが、何かの参考になればと思う。
associateなど sequelize の使い方に関する説明は今後ちゃんと記述して説明していこうと思う。