44
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

sequelize v5とtypescript

Last updated at Posted at 2019-07-07

はじめに

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() みたいな感じで使えば、冒頭に紹介したような感じで推論してくれるようになってます。

44
31
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
44
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?