はじめに
Node.jsを使う場合、ORMにsequelizeを使うことが多いと思います。
そのsequelizeをtypescriptでちゃんと型定義してあげると、
sequelizeのモデルに生えているメソッドを推論してくれたり、
findOneなどの引数に渡す検索のoptionの推論をしてくれたり、
取ってきたデータがどんなカラムを持っているのかがわかったりします。
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で定義したプロパティが、推論を行なってくれるカラムになってきます。
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の場合もあるカラム。
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できるファイル
このファイルは型定義のために使ったり、モデルの作成のために使います。
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が使えるようになる)
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
/* 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
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() みたいな感じで使えば、冒頭に紹介したような感じで推論してくれるようになってます。