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 を作成する。 それぞれの仕様は下図の通り。

f:id:poppon555:20190721132919p:plain

両 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 の使い方に関する説明は今後ちゃんと記述して説明していこうと思う。