Edited at

sequelize v5とtypescript


はじめに

Node.jsを使う場合、ORMにsequelizeを使うことが多いと思います。

そのsequelizeをtypescriptでちゃんと型定義してあげると、

sequelizeのモデルに生えているメソッドを推論してくれたり、

スクリーンショット 2019-07-07 13.28.40.png

findOneなどの引数に渡す検索のoptionの推論をしてくれたり、

スクリーンショット 2019-07-07 13.31.08.png

取ってきたデータがどんなカラムを持っているのかがわかったりします。

スクリーンショット 2019-07-07 13.32.44.png

sequelizeとtypescriptを組み合わせるには、sequelize-typescriptを使えばできたりしますが、sequelize v5からは、公式がtypescriptをサポートしてくれています。

その公式のやり方を使って、sequelizeとtypescriptを組み合わせてみます。


前提

sequelizeを使う場合、 俺が作った最高のmodelGenerator みたいなコードができがちです。

今回、ここに載せた例も一例に過ぎないので作り方は色々あります。

HasManyGetAssociationsMixin とか適当なsequelizeのメソッド名でgithubを検索すれば、世の中の人がどんな感じでsequelizeとtypescriptを組み合わせているのか、例がたくさん出てきます。


コード


モデル定義

usersとuser_detailsというテーブルがあると仮定して、usersとuser_detailsが一対一で繋がっているようなモデルを定義していきます。

src

├── models
│ ├── hoge
│ │ ├── index.ts
│ │ ├── schema.ts
│ │ ├── user_details.ts
│ │ └── users.ts


usersのモデル定義

user_detailsというテーブルと一対一で繋がる。

interfaceの末尾に!をつけるとそのカラムはnullになることはないという意味。!についての詳細はこちら

このモデルのclassで定義したプロパティが、推論を行なってくれるカラムになってきます。


src/models/hoge/users.ts


import { Sequelize, Model, DataTypes } from 'sequelize';
import { UserDetails } from './user_details';

const TABLE_NAME = 'users';

class Users extends Model {
public id!: number;
public email!: string;
public created_at!: Date;
public updated_at!: Date;
public user_detail!: UserDetails;

public static attach(sequelize: Sequelize): void {
this.init(
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
allowNull: false,
primaryKey: true,
},
email: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: '',
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
},
},
{
tableName: TABLE_NAME,
underscored: true,
sequelize: sequelize,
}
);
}

public static associate(): void {
Users.hasOne(UserDetails, {
foreignKey: 'user_id',
});
}
}

const factory = (sequelize: Sequelize) => {
Users.attach(sequelize);

return Users;
};

export { Users, factory };



user_detailsのモデル定義

interfaceの末尾に?がついたカラムはnullの場合もあるカラム。


src/models/hoge/user_details.ts


import { Sequelize, Model, DataTypes } from 'sequelize';
import { Users } from './users';

const TABLE_NAME = 'user_details';

class UserDetails extends Model {
public id!: number;
public user_id!: number;
public nick_name?: string;
public first_name?: string;
public last_name?: string;
public created_at!: Date;
public updated_at!: Date;

public static attach(sequelize: Sequelize): void {
this.init(
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
allowNull: false,
primaryKey: true,
},
user_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
nick_name: {
type: DataTypes.STRING,
allowNull: true,
},
first_name: {
type: DataTypes.STRING,
allowNull: true,
},
last_name: {
type: DataTypes.STRING,
allowNull: true,
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
},
},
{
tableName: TABLE_NAME,
underscored: true,
sequelize: sequelize,
}
);
}

public static associate(): void {
UserDetails.hasOne(Users, {
foreignKey: 'user_id',
as: 'user_detail',
});
}
}

const factory = (sequelize: Sequelize) => {
UserDetails.attach(sequelize);

return UserDetails;
};

export { UserDetails, factory };



各モデルをまとめてimportできるファイル

このファイルは型定義のために使ったり、モデルの作成のために使います。


src/models/hoge/schema.ts

import * as users from './users';

import * as userDetails from './user_details';

export default {
users,
user_details: userDetails
};



dbアクセス時のエントリーポイントとなるファイル

dbにアクセスしたい際は、このファイルをimportします。

model_generatorは setModel() を受け取るようにしておき、使う側が利用するモデル群をハンドリングできるようにしました。(そうしておけば例えば、dbが違う別サービスでも共通したmodel_generatorが使えるようになる)


src/models/hoge/index.ts

import { Sequelize } from 'sequelize';

import { hogeModelGenerate } from '../../utils/model_generator';
import dbConfig from '../../config/db';
import schema from '../../models/hoge/schema';

const setModel = (sequelize: Sequelize): HogeDB => {
const db: any = {};

Object.keys(schema).forEach(tableName => {
db[tableName] = schema[tableName].factory(sequelize);
});

// associationを貼るのは各Modelのinit()が全て終わってから
// (全モデルのinit()が終わる前にassociationを貼るとそんなモデル知らないみたいなエラーで死ぬ)
Object.keys(schema).forEach(tableName => {
if ('associate' in db[tableName]) {
db[tableName].associate(db);
}
});

return db;
};

const modelGenerator = hogeModelGenerate(dbConfig.service);
const db = modelGenerator(setModel);

export default db;



モデルの型定義

モデルの型を定義します。

@typesディレクトリはtsconfigのtypeRootsに指定しておき、グローバルに読めるようにしておく)

src

├── @types
│ ├── hoge
│ │ └── index.d.ts


src/@types/hoge/index.d.ts


/* eslint @typescript-eslint/camelcase: 0 */
import schema from '../../../models/hoge/schema';

declare interface HogeDB {
users: typeof schema.users.Users;
user_details: typeof schema.users.UserDetails;
}



モデルをgenerateするファイル

Sequelizeのインスタンスを作って各モデルのinitをします。

このmodel_generatorを使う側は、 setModel() の実装を渡してもらう必要があります。

src

└── utils
└── model_generator.ts


src/utils/model_generator.ts

import { Sequelize, Op } from 'sequelize';

export default class ModelGenerater {
public sequelize: Sequelize;

public constructor(dbConfig: any) {
this.sequelize = new Sequelize(dbConfig.database, dbConfig.username, dbConfig.password, {
host: dbConfig.host,
dialect: 'mysql',
port: 3306,
logging: true,
omitNull: true,
});
}
}

export const hogeModelGenerate = (dbConfig: any) => {
const modelGenerator = new ModelGenerater(dbConfig);

return (setModel: any) => {
const db: HogeDB = setModel(modelGenerator.sequelize);

return {
...db,
Sequelize,
sequelize: modelGenerator.sequelize,
Op,
};
};
};


constructorに渡しているdbConfigはこんな感じの、DB周りの設定を環境変数から読み込むようなやつです。


export default {
host: process.env.HOGE_SERVICE_DB_HOST || '127.0.0.1',
username: process.env.HOGE_SERVICE_DB_USER || 'root',
password: process.env.HOGE_SERVICE_DB_PASSWORD || '',
database: process.env.HOGE_SERVICE_DB_DATABASE || 'hoge',
};


使い方

src/models/hoge/index.ts をimportして

db.users.findOne() みたいな感じで使えば、冒頭に紹介したような感じで推論してくれるようになってます。