Chapter 22

techniques-database

kisihara.c
kisihara.c
2021.03.14に更新

データベース

Nestはデータベース不可知であり、任意のSQLやNoSQLと簡単に統合できる。好みに応じて、いくらかの選択肢がある。最も一般的なレベルでNestをデータベースに接続するなら、ExpressやFastifyと同様、データベース用の適切なNode.jsドライバを読み込むだけだ。

また、より抽象度の高い操作を行う為に、Sequelize(下記でも統合方法を説明している)、Knex.jsチュートリアル)、TypeORMPrisma(recipeチャプターで詳細説明)等の汎用的Node.jsデータベース統合ライブラリ・ORMをダイレクトに使用する事もできる。

便利なように、NestはTypeORMとSequelize、Mongooseとの緊密なインテグレーションを提供しており、それぞれ@nestjs/typeorm@nestjs/sequelize@nestjs/mongooseパッケージですぐに利用できる。これらの統合により、モデル/リポジトリのインジェクション、テスト可能性、非同期設定等NestJS特有の機能が追加され、選択したデータベースへのアクセスをより簡単に行える。

TypeORMのインテグレーション

SQLやNoSQLデータベースとの統合のために、Nestは@nestjs/typeormパッケージを提供している。NestがTypeORMを使用しているのは、TypeScriptで利用できる最も成熟したORMだからだ。TypeScriptで書かれているがゆえに、Nestフレームワークと上手く統合できる。

まず必要な依存関係をインストールしよう。この章では一般的なMySQLRDB管理システムでデモを行うが、TypeORMはPostgreSQL、Oracle、Microsoft SQL Server、SQLite、さらにはMongoDBのようなNoSQLデータベースなど、多くのリレーショナル・データベースをサポートしている。この章の手順はTypeORMでサポートされているどのデータベースでも同じだ。必要なのは、選択したデータベースに関連する、クライアントAPIをインストールする事だけ。

$ npm install --save @nestjs/typeorm typeorm mysql2

インストールが完了したら、ルートのAppModuleにTypeOrmModuleをインポートする。

app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [],
      synchronize: true,
    }),
  ],
})
export class AppModule {}

Warning
静的なglob path(例:dist/**/*.entity{ .ts,.js})はWebpackでは正しく動作しない。

HINT
ormconfig.jsonファイルはtypeormライブラリによって読み込まれることに注意してほしい。したがって、上記で説明した追加のプロパティ(forRoot()メソッドによって内部的にサポートされているもの。例:autoLoadEntitiesretryDelay)は適用されない。幸い、TypeORMは接続オプションをORMconfigファイルや環境変数から読み込むgetConnectionOptions関数を提供している。この関数を使う事でも、以下のように設定ファイルからNest特有のオプションを設定できる。

TypeOrmModule.forRootAsync({
useFactory: async () =>
  Object.assign(await getConnectionOptions(), {
    autoLoadEntities: true,
  }),
})

これが完了すると、TypeORMのConnectionEntytyManagerオブジェクトはプロジェクト全体にインジェクション出来るようになる(モジュールをインポートする必要はない)。

app.module.ts
import { Connection } from 'typeorm';

@Module({
  imports: [TypeOrmModule.forRoot(), UsersModule],
})
export class AppModule {
  constructor(private connection: Connection) {}
}

Repositoryパターン

TypeORMはRepositoryパターンをサポートしているので、各エンティティは独自のリポジトリを持っている。データベース接続を通して取得できる。

例を続ける為には最低1つのエンティティが必要だ。Userエンティティを定義しよう。

user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column({ default: true })
  isActive: boolean;
}

HINT
エンティティについては、TypeORMのドキュメントを参照のこと。

Userエンティティファイルは、usersディレクトリにある。このディレクトリにはUsersModuleに関連するすべてのファイルが含まれている。モデルファイルをどこに保存するかは自由に決められるが、モデルファイルはそのドメインの近く、対応するモジュールディレクトリに作成する事を勧める。

Userエンティティの使用を開始するには、モジュールのforRoot()メソッドオプションのentities配列に挿入してTypeORMに読み込ませる必要がある(静的なglob pathを使っている場合を除く)。

app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './users/user.entity';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [User],
      synchronize: true,
    }),
  ],
})
export class AppModule {}

次に、UsersModuleを見てみよう。

users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { User } from './user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UsersService],
  controllers: [UsersController],
})
export class UsersModule {}

このモジュールはforFeature()メソッドを用いて、現在のスコープに登録されているリポジトリを定義する。結果、@InjectRepository()デコレータを使用して、UsersRepositoryUsersServiceにインジェクションする事ができる。

users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  findAll(): Promise<User[]> {
    return this.usersRepository.find();
  }

  findOne(id: string): Promise<User> {
    return this.usersRepository.findOne(id);
  }

  async remove(id: string): Promise<void> {
    await this.usersRepository.delete(id);
  }
}

NOTICE
ルートのAppModuleUserModuleをインポートする事を忘れない事。

TypeOrmModule.forFeatureをインポートしたモジュール以外のリポジトリを使用したい場合は、生成されたプロバイダを再インポートする必要がある。これは、以下のようにモジュール全体をエクスポートする事で行える。

users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  exports: [TypeOrmModule]
})
export class UsersModule {}

さて、UserHttpModuleUsersModuleをインポートすると、後者に属するプロバイダで@InjectRepository(User)を使うことができる。

users-http.module.ts

import { Module } from '@nestjs/common';
import { UsersModule } from './users.module';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';

@Module({
  imports: [UsersModule],
  providers: [UsersService],
  controllers: [UsersController]
})
export class UserHttpModule {}

リレーション

リレーションとは、2つ以上のテーブル間の関係性の事だ。リレーションは各テーブルの共通フィールドに基づいており、多くの場合主キーと外部キーが関係している。

関係には3つのタイプがある。

一対一 主テーブルのすべての行が、外部テーブルの関連行を一つだけ持っている。このタイプのリレーションを定義するには@OneToOne()デコレータを使用する。
一対多/多対一 主テーブル内のすべての行が外部テーブル内に、1つ以上の関連行を持つ。@OneToMany()@ManyToOne() を使う。
多対多 主テーブルの全ての行が外部テーブルの中に多くの関連行を持ち、外部テーブルの全ての行が主テーブルの中に多くの関連行を持つ。@ManyToManyデコレータを使用する。

エンティティ内の関係を定義するには、対応するデコレータを使用する。例えば各ユーザが複数の写真を持つ場合、@OneToMany()デコレータを使用する。

user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import { Photo } from '../photos/photo.entity';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column({ default: true })
  isActive: boolean;

  @OneToMany(type => Photo, photo => photo.user)
  photos: Photo[];
}

HINT
TypeORMのリレーションについての詳細は、TypeORMのドキュメントを参照の事。

オートロードエンティティ

接続オプションのentities配列にエンティティを手動で追加するのは面倒だ。さらに、ルートモジュールからエンティティを参照すると、アプリケーションのドメイン境界が破られて、アプリケーションの他部分に実装の詳細が漏れる原因となる。この問題を解決するために、静的なglob pathを使用できる。(例:dist/**/*.entity{ .ts,.js}

しかしながら、glob pathはwebpackではサポートされていない。アプリケーションをmonorepoの仲で構築している場合は使えない。代わりに別の解決策が用意されている。エンティティをオートロードするには、以下に示すように、設定オブジェクト(forRoot()メソッドに渡される)のautLoadEntitiesプロパティをtrueに設定する。

app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      ...
      autoLoadEntities: true,
    }),
  ],
})
export class AppModule {}

このオプションを指定すると、forFeature()メソッドで登録された全てのエンティティが、設定オブジェクトのentities配列に自動で追加される。

WARNING
autoLoadEntitiesの設定によっては、forFeature()メソッドで登録されておらずエンティティから(リレーションシップを通して)参照されているだけのエンティティには適用されない。

エンティティの定義を分ける

デコレータを使用すれば、モデル内でエンティティとその行を定義できる。だが、「エンティティスキーマ」を利用して、別のファイル内でエンティティとその行を定義したい人もいるだろう。

import { EntitySchema } from 'typeorm';
import { User } from './user.entity';

export const UserSchema = new EntitySchema<User>({
  name: 'User',
  target: User,
  columns: {
    id: {
      type: Number,
      primary: true,
      generated: true,
    },
    firstName: {
      type: String,
    },
    lastName: {
      type: String,
    },
    isActive: {
      type: Boolean,
      default: true,
    },
  },
  relations: {
    photos: {
      type: 'one-to-many',
      target: 'Photo', // the name of the PhotoSchema
    },
  },
});

WARNING
targetオプションを指定した場合、nameオプションの値はターゲットクラスの名前と同じでなければならない。targetを指定しない場合は任意の名前を使用可能。

Nestでは、Entityを置ける場所ならどこでも(wherever an Entity is expected)EntitySchemeインスタンスを使用可能。

例:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserSchema } from './user.schema';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  imports: [TypeOrmModule.forFeature([UserSchema])],
  providers: [UsersService],
  controllers: [UsersController],
})
export class UsersModule {}

トランザクション

データベーストランザクションとは、データベース管理システム内でデータベースに対して実行される作業の単位を表す(詳細)。そして、他のトランザクションとは独立し首尾一貫している信頼性の高い形で実行される。

TypeORMのトランザクションを扱うためには、多くの異なる戦略がある。オススメはQueryRunnerクラスだ。トランザクションについての完全なコントロールが行える。

まず通常の方法でConnectionオブジェクトをクラスにインジェクションする必要がある。

@Injectable()
export class UsersService {
  constructor(private connection: Connection) {}
}

HINT
Connectionクラスはtypeormパッケージからインポートする。

ではこのオブジェクトを使ってトランザクションを作成しよう。


async createMany(users: User[]) {
  const queryRunner = this.connection.createQueryRunner();

  await queryRunner.connect();
  await queryRunner.startTransaction();
  try {
    await queryRunner.manager.save(users[0]);
    await queryRunner.manager.save(users[1]);

    await queryRunner.commitTransaction();
  } catch (err) {
    // エラーが発生したので変更をロールバック
    await queryRunner.rollbackTransaction();
  } finally {
    // 手動でインスタンス化されたqueryRunnerをreleaseする必要がある
    await queryRunner.release();
  }
}

HINT
connectionQueryRunnerの為だけに作られる事に注意。しかし、このクラスのテストのためにはConnectionオブジェクト全体をモックする必要がある(複数のメソッドを表出している)。そこで、ヘルパーファクトリクラス(例:QueryRunnerFactory)を使い、トランザクションを可能にするために必要なメソッドの特定のセットを持つ、インターフェイスを定義する事を勧める。このテクニックを使うと、メソッドのモックがすごく簡単になる。

もしくはConnectionオブジェクトのトランザクションメソッドを使用してコールバックスタイルのアプローチを使用する事もできる(詳細)。

async createMany(users: User[]) {
  await this.connection.transaction(async manager => {
    await manager.save(users[0]);
    await manager.save(users[1]);
  });
}

デコレータを使ってトランザクションを制御する(@Transaction()TransactionManager())のは勧められない。

サブスクライバ

TypeORMのサブスクライバを使うと特定のエンティティイベントをlistenできる。

import {
  Connection,
  EntitySubscriberInterface,
  EventSubscriber,
  InsertEvent,
} from 'typeorm';
import { User } from './user.entity';

@EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<User> {
  constructor(connection: Connection) {
    connection.subscribers.push(this);
  }

  listenTo() {
    return User;
  }

  beforeInsert(event: InsertEvent<User>) {
    console.log(`BEFORE USER INSERTED: `, event.entity);
  }
}

WARNING
イベントサブスクライバはリクエストスコープ化できない。

providers配列にUserSubscriberクラスを追加しよう。

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { UserSubscriber } from './user.subscriber';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UsersService, UserSubscriber],
  controllers: [UsersController],
})
export class UsersModule {}

HINT
エンティティサブスクライバの詳細はこちら

マイグレーション

マイグレーションはデータベース内の既存のデータを保持しつつ、アプリケーションのデータモデルと動悸させる為、データベーススキーマを段階的に更新する方法を提供する。マイグレーションの生成・実行・復帰の為に、TypeORMは専用のCLIを提供する。

マイグレーションクラスはNestアプリケーションのソースコードから分離されている。そのライフサイクルはTypeORM CLIによって管理される。したがって、依存性のインジェクションやその他Nest特有の昨日をマイグレーションで利用する事はできない。マイグレーションの詳細は、TypeORMのドキュメントにて。

マルチプルデータベース

プロジェクトによっては複数のデータベース接続が必要となる事もある。このモジュールを使えば実現できる。複数の接続を使用するには、まず接続を作成する。この場合、接続の命名が必須となる。

独自のデータベースに保存されているAlbumエンティティがあるとする。

const defaultOptions = {
  type: 'postgres',
  port: 5432,
  username: 'user',
  password: 'password',
  database: 'db',
  synchronize: true,
};

@Module({
  imports: [
    TypeOrmModule.forRoot({
      ...defaultOptions,
      host: 'user_db_host',
      entities: [User],
    }),
    TypeOrmModule.forRoot({
      ...defaultOptions,
      name: 'albumsConnection',
      host: 'album_db_host',
      entities: [Album],
    }),
  ],
})
export class AppModule {}

NOTICE
接続に名前を設定しない場合、defaultの接続に設定される。名前を設定しないまま、もしくは同じ名前で複数の接続を持つべきではない。

この時点で、UserAlbumeのエンティティが独自の接続で登録されている。TypeOrmModule.forFeature()メソッドと@InjectRepository()デコレータに対してどの接続を使用するか、指定しなければならない。接続名を渡さない場合はdefaultに設定される。

@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    TypeOrmModule.forFeature([Album], 'albumsConnection'),
  ],
})
export class AppModule {}

また、与えられた接続のConnectionEntityManagerをインジェクションする事もできる。

@Injectable()
export class AlbumsService {
  constructor(
    @InjectConnection('albumsConnection')
    private connection: Connection,
    @InjectEntityManager('albumsConnection')
    private entityManager: EntityManager,
  ) {}
}

プロバイダに任意のConnectionをインジェクションする事も可能だ。

@Module({
  providers: [
    {
      provide: AlbumsService,
      useFactory: (albumsConnection: Connection) => {
        return new AlbumsService(albumsConnection);
      },
      inject: [getConnectionToken('albumsConnection')],
    },
  ],
})
export class AlbumsModule {}

テスト

アプリケーションの単体テストを行う場合、通常はデータベースへの接続を避け、テストスイートを独立させて実行プロセスを可能な限り保ちたくなる。しかし我々のクラスが接続インスタンスから引き出されるリポジトリに依存している場合もある。どうすればいいだろう? 解決策はモックリポジトリを作成する事だ。カスタムプロバイダを設定しよう。登録された各リポジトリは自動的に[<EntytyName>Repository]として表出する。

@nestjs/typeormパッケージは与えられたエンティティに基づいて用意されたトークンを返すgetRepositoryToken()関数を持っている。

@Module({
  providers: [
    UsersService,
    {
      provide: getRepositoryToken(User),
      useValue: mockRepository,
    },
  ],
})
export class UsersModule {}

これで、mockRepositoryUsersRepositoryの代替として使用されるようになる。任意のクラスが@InjectRepository()デコレータを使用してUsersRepositoryを要求すると、Nestは登録されたmockRepoitoryオブジェクトを使用する。

カスタムリポジトリ

TypeORMはカスタムリポジトリという機能を提供している。基礎となるリポジトリクラスを拡張したり、いくつかの特別なメソッドを使って機能強化できる。詳細はこちら

カスタムリポジトリを作るためには、@EntityRepository()デコレータを使い、Repositoryクラスを拡張する。

@EntityRepository(Author)
export class AuthorRepository extends Repository<Author> {}

HINT
@EntityRepository()Repositoryはそれぞれtypeormパッケージからインポートしている。

クラスを作成したら、次はインスタンス化の責任をNestに委譲する。そのためには、TypeOrm.forFeature()メソッドにAuthorRepositoryクラスを渡す必要がある。

@Module({
  imports: [TypeOrmModule.forFeature([AuthorRepository])],
  controller: [AuthorController],
  providers: [AuthorService],
})
export class AuthorModule {}

後は、以下のような構成でリポジトリをインジェクションするだけだ。

@Injectable()
export class AuthorService {
  constructor(private authorRepository: AuthorRepository) {}
}

Asyncの設定

リポジトリモジュールのオプションを静的に渡すのではなく、非同期に渡したい場合があるかもしれない。この場合はforRootAsync()メソッドを使用する。asyncの設定に対して複数の方法を提供するメソッドだ。

まずファクトリー関数を使う方法がある。

TypeOrmModule.forRootAsync({
  useFactory: () => ({
    type: 'mysql',
    host: 'localhost',
    port: 3306,
    username: 'root',
    password: 'root',
    database: 'test',
    entities: [__dirname + '/**/*.entity{.ts,.js}'],
    synchronize: true,
  }),
});

ファクトリーは他の非同期プロバイダと同じように動作する(例:非同期にでき、依存性のインジェクションが可能)。

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: (configService: ConfigService) => ({
    type: 'mysql',
    host: configService.get('HOST'),
    port: +configService.get<number>('PORT'),
    username: configService.get('USERNAME'),
    password: configService.get('PASSWORD'),
    database: configService.get('DATABASE'),
    entities: [__dirname + '/**/*.entity{.ts,.js}'],
    synchronize: true,
  }),
  inject: [ConfigService],
});

あるいは、useClass構文を使う事もできる。

TypeOrmModule.forRootAsync({
  useClass: TypeOrmConfigService,
});

上記のコードでは、TypeOrmModule内にTypeOrmConfigServiceをインスタンス化し、それを使用してcreateTypeOrmOptions()を呼び出してオプションオブジェクトを提供している。これは、以下に示すように、TypeOrmConfigServiceTypeOrmOptionsFactoryインターフェイスを実装しなければならない事を意味する。

@Injectable()
class TypeOrmConfigService implements TypeOrmOptionsFactory {
  createTypeOrmOptions(): TypeOrmModuleOptions {
    return {
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: true,
    };
  }
}

TypeOrmModule内でのTypeOrmConfigServiceの生成を止めて別のモジュールからインポートしたプロバイダを使用するには、useExisting構文を使用する。

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  useExisting: ConfigService,
});

このコードはuseClassと同様に動作する。重要なのは、TypeOrmModuleがインポートされたモジュールを検索し、既存のConfigServiceを再利用する事だ。

HINT
nameプロパティがuseFactoryuseClassuseValueプロパティと同じレベルで定義されている事を確認の事。これにより、Nestは適切なインジェクショントークンの下で適切に接続を登録できる。

動くサンプルはこちら

Sequelizeのインテグレーション

もう一つの選択肢として、@nestjs/sequelizeパッケージからORM Sequelizeを使用可能。加えてここでは、エンティティを宣言的に定義するためのデコレータを追加で提供しているsequelize-typescript を使用する。

まず必要な依存関係をインストールしよう。この章では一般的なMySQLを使用するが、SequelizeはPostgreSQL、MySQL、Microsoft SQL Server、SQLite、MariaDB等多くのデータベースをサポートしている。説明する手順はどのデータベースでも変わらない。選択したデータベースに関連するクライアントAPIライブラリをインストールするだけだ。

$ npm install --save @nestjs/sequelize sequelize sequelize-typescript mysql2
$ npm install --save-dev @types/sequelize

インストールが完了したら、ルートのAppModuleにSequelizeModuleをインポートする。

import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';

@Module({
  imports: [
    SequelizeModule.forRoot({
      dialect: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      models: [],
    }),
  ],
})
export class AppModule {}

forRoot()メソッドはSequelizeのコンストラクタで公開されている全ての設定プロパティをサポートしている(詳細)。加えて、以下のいくつかの追加設定プロパティがある。

retryAttempts データベースへの接続試行回数(デフォルト:10
retryDelay 接続を再試行するまでの時間(ms)(デフォルト:3000
autoLoadModels true時モデルは自動的にロードされる(デフォルト:false
keepConnectionAlive true時、アプリケーションのシャットダウン時に接続を閉じない(デフォルトはfalse)
synchronize true時、自動的にロードされたモデルが同期される(デフォルト:false

これが完了すれば、(モジュールを全くインポートする必要なく(without needing to import any module))Sequelizeオブジェクトがプロジェクト全体を通してインジェクション可能となる。

例:

import { Injectable } from '@nestjs/common';
import { Sequelize } from 'sequelize-typescript';

@Injectable()
export class AppService {
  constructor(private sequelize: Sequelize) {}
}

モデル

SequelizeはActive Recordパターンを実装している。このパターンではモデルクラスを直接使用してデータベースと対話する。例を挙げるには少なくとも1つモデルが必要だ。Userモデルを定義してみよう。

user.model.ts
import { Column, Model, Table } from 'sequelize-typescript';

@Table
export class User extends Model<User> {
  @Column
  firstName: string;

  @Column
  lastName: string;

  @Column({ defaultValue: true })
  isActive: boolean;
}

HINT
利用可能なデコレータについての詳細

Userモデルのファイルはusersディレクトリに置かれる。このディレクトリにはUsersModuleに関するすべてのファイルが含まれている。モデルファイルをどこに置くかは自由だが、ドメインの近く、つまり対応するモジュールのディレクトリに作成することを勧める。

Userモデルを使用するには、モジュールのforRoot()メソッドのオプションの中でmodels配列にそれを挿入して、Sequelizeで読む込む必要がある。

app.module.ts
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './users/user.model';

@Module({
  imports: [
    SequelizeModule.forRoot({
      dialect: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      models: [User],
    }),
  ],
})
export class AppModule {}

次に、UsersModuleを見てみよう。

users.module.ts
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './user.model';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  imports: [SequelizeModule.forFeature([User])],
  providers: [UsersService],
  controllers: [UsersController],
})
export class UsersModule {}

このモジュールでは、foreFeature()メソッドを使用して、現在のスコープに登録されるモデルを定義している。そうすれば、@InjectModel()デコレータを使ってUserModelUsersServiceにインジェクションできる。

users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { User } from './user.model';

@Injectable()
export class UsersService {
  constructor(
    @InjectModel(User)
    private userModel: typeof User,
  ) {}

  async findAll(): Promise<User[]> {
    return this.userModel.findAll();
  }

  findOne(id: string): Promise<User> {
    return this.userModel.findOne({
      where: {
        id,
      },
    });
  }

  async remove(id: string): Promise<void> {
    const user = await this.findOne(id);
    await user.destroy();
  }
}

NOTICE
ルートのAppModuleUsersModuleをインポートする事を忘れないでほしい。

SequelizeModule.forFeatureをインポートしているモジュールの外でリポジトリを使用したい場合は、生成されたプロバイダを再インポートする必要がある。その為には、次のようにモジュール全体をエクスポートする。

users.module.ts
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './user.entity';

@Module({
  imports: [SequelizeModule.forFeature([User])],
  exports: [SequelizeModule]
})
export class UsersModule {}

ここで、UserHttpModuleUsersModuleをインポートすると、@InjectModel(User)が使えるようになる。

users-http.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users.module';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';

@Module({
  imports: [UsersModule],
  providers: [UsersService],
  controllers: [UsersController]
})
export class UserHttpModule {}

リレーション

リレーションとは、2つ以上のテーブル間の関係性の事だ。リレーションは各テーブルの共通フィールドに基づいており、多くの場合主キーと外部キーが関係している。

関係には3つのタイプがある。

(同上、略)

エンティティに関係を定義する為には、対応するデコレータを使用する。たとえば、各Userが複数の写真を持つ事ができると定義する際は@HasMany()デコレータを使用する。

user.entity.ts
import { Column, Model, Table, HasMany } from 'sequelize-typescript';
import { Photo } from '../photos/photo.model';

@Table
export class User extends Model<User> {
  @Column
  firstName: string;

  @Column
  lastName: string;

  @Column({ defaultValue: true })
  isActive: boolean;

  @HasMany(() => Photo)
  photos: Photo[];
}

HINT
Sequelizeのassociationについての詳細

モデルの自動読み込み

接続時の引数のmodels配列に手作業でモデルを追加していくのは面倒だ。またルートモジュールからモデルを参照すると、アプリケーションのドメイン境界が崩れ、実装の詳細が表出されてしまう。以下のように、(forRoot()メソッドに渡される)設定オブジェクトのautoLoadModelsプロパティとsynchronizeプロパティをtrueに設定して、モデルを自動的にロードする。

app.module.ts
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';

@Module({
  imports: [
    SequelizeModule.forRoot({
      ...
      autoLoadModels: true,
      synchronize: true,
    }),
  ],
})
export class AppModule {}

このオプションを指定すると、forFeature()メソッドで登録した全てのモデルが、設定オブジェクトのmodels配列に自動的に追加される。

WARNING
forFeature()メソッドで登録されたモデルではなく、モデルから(関連付けを通して)参照されているだけのモデルは含まれないことに注意。

トランザクション

データベーストランザクションとは、データベース管理システム内でデータベースに対して実行される作業の単位を表す(詳細)。そして、他のトランザクションとは独立し首尾一貫している信頼性の高い形で実行される。

Sequelizeのトランザクションを扱うためには、多くの異なる戦略がある。以下はマネージドトランザクション(自動コールバック)の例だ。

最初に、Sequlizeオブジェクトをクラスに通常通りインジェクションする。

@Injectable()
export class UsersService {
  constructor(private sequelize: Sequelize) {}
}

HINT
Sequelizeクラスはsequelize-typescriptパッケージからインポートしている。

では、このオブジェクトを使ってトランザクションを作成する。

async createMany() {
  try {
    await this.sequelize.transaction(async t => {
      const transactionHost = { transaction: t };

      await this.userModel.create(
          { firstName: 'Abraham', lastName: 'Lincoln' },
          transactionHost,
      );
      await this.userModel.create(
          { firstName: 'John', lastName: 'Boothe' },
          transactionHost,
      );
    });
  } catch (err) {
    // トランザクションがロールバックされる
    // プロミスチェーンを拒否しトランザクションコールバックを返したものはすべてエラーとなる
    // (err is whatever rejected the promise chain returned to the transaction callback)
    // (deepL:errは、トランザクションコールバックに返されたプロミスチェーンが拒否されたものです。)
  }
}

Sequelizeインスタンスは、トランザクションを開始するためにのみ使用される事に注意。しかし、このクラスをテストするには、(メソッドを表出する)Sequelizeオブジェクト全体をモックする必要がある。そこで、ヘルパーファクトリクラス(例:TransactionRunner)を使用して、トランザクションを維持するために必要な限られたメソッドのセットを持つ、インターフェイスを定義しよう。メソッドのモックが非常に簡単になる。

マイグレーション

マイグレーションはデータベース内の既存のデータを保持しつつ、アプリケーションのデータモデルと動悸させる為、データベーススキーマを段階的に更新する方法を提供する。マイグレーションの生成・実行・復帰の為に、Sequelizeは専用のCLIを提供する。

マイグレーションクラスはNestアプリケーションのソースコードから分離されている。そのライフサイクルはSequelize CLIによって管理される。したがって、依存性のインジェクションやその他Nest特有の昨日をマイグレーションで利用する事はできない。マイグレーションの詳細は、Sequelizeのドキュメントにて。

マルチプルデータベース

プロジェクトによっては、複数のデータベース接続が必要になる場合がある。まず接続を作成しよう。この場合名前付けが必須となる。

例えば、独自のデータベースに保存されているAlbumのエンティティがあるとする。

const defaultOptions = {
  dialect: 'postgres',
  port: 5432,
  username: 'user',
  password: 'password',
  database: 'db',
  synchronize: true,
};

@Module({
  imports: [
    SequelizeModule.forRoot({
      ...defaultOptions,
      host: 'user_db_host',
      models: [User],
    }),
    SequelizeModule.forRoot({
      ...defaultOptions,
      name: 'albumsConnection',
      host: 'album_db_host',
      models: [Album],
    }),
  ],
})
export class AppModule {}

NOTICE
接続に名前を設定しない場合、defaultの接続に設定される。名前を設定しないまま、もしくは同じ名前で複数の接続を持つべきではない。

この時点で、UserモデルとAlbumモデルがそれぞれのコネクションに接続されている。この状態で、SequelizeModule.forFeature()メソッドと@InjectModel()デコレータにどの接続を使うか伝える必要がある。接続名を渡さなかった場合はdefaultの接続が使用される。

@Module({
  imports: [
    SequelizeModule.forFeature([User]),
    SequelizeModule.forFeature([Album], 'albumsConnection'),
  ],
})
export class AppModule {}

また、特定の接続に対してSequelizeインスタンスをインジェクションすることもできる。

@Injectable()
export class AlbumsService {
  constructor(
    @InjectConnection('albumsConnection')
    private sequelize: Sequelize,
  ) {}
}

どんなSequelizeインスタンスもプロバイダに注入する事ができる。

@Module({
  providers: [
    {
      provide: AlbumsService,
      useFactory: (albumsSequelize: Sequelize) => {
        return new AlbumsService(albumsSequelize);
      },
      inject: [getConnectionToken('albumsConnection')],
    },
  ],
})
export class AlbumsModule {}

テスト

アプリケーションの単体テストを行う場合、通常はデータベースへの接続を避け、テストスイートを独立させて実行プロセスを可能な限り保ちたくなる。しかし我々のクラスが接続インスタンスから引き出されるリポジトリに依存している場合もある。どうすればいいだろう? 解決策はモックリポジトリを作成する事だ。カスタムプロバイダを設定しよう。登録された各リポジトリは自動的に<ModelName>Modelトークンとして表出する。

@nestjs/sequelizeパッケージは与えられたエンティティに基づいて用意されたトークンを返すgetModelToken()関数を持っている。

@Module({
  providers: [
    UsersService,
    {
      provide: getModelToken(User),
      useValue: mockModel,
    },
  ],
})
export class UsersModule {}

これでmockModelUserModelとして使用されるようになった。すべてのクラスで、InjectModel()デコレータを使ってUserModelを要求した時には、登録されたmockModelオブジェクトが使われる。

Asyncの設定

SequelizeModuleのオプションを静的にではなく非同期に渡したい場合がある。この場合はforRootAsync()メソッドを使う。forRootAsync()メソッドには、非同期設定を扱う方法がいくつか用意されている。

一つの方法はファクトリー関数を使うことだ。

SequelizeModule.forRootAsync({
  useFactory: () => ({
    dialect: 'mysql',
    host: 'localhost',
    port: 3306,
    username: 'root',
    password: 'root',
    database: 'test',
    models: [],
  }),
});

ファクトリーは他の非同期プロバイダと同じように動作する(例:非同期にする事もできるし、インジェクションで依存関係を注入する事もできる)

SequelizeModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: (configService: ConfigService) => ({
    dialect: 'mysql',
    host: configService.get('HOST'),
    port: +configService.get('PORT'),
    username: configService.get('USERNAME'),
    password: configService.get('PASSWORD'),
    database: configService.get('DATABASE'),
    models: [],
  }),
  inject: [ConfigService],
});

useClass構文を使う事もできる。

SequelizeModule.forRootAsync({
  useClass: SequelizeConfigService,
});

上記のコードでは、SequelizeModule内でSequelizeConfigServiceをインスタンス化し、それを使用してcreateSequelizeOptions()を呼び出してオプションオブジェクトを提供する。この場合、SequelizeConfigServiceは以下のようにSequelizeOptionsFactoryインターフェイスを実装する必要がある事に注意。

@Injectable()
class SequelizeConfigService implements SequelizeOptionsFactory {
  createSequelizeOptions(): SequelizeModuleOptions {
    return {
      dialect: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      models: [],
    };
  }
}

SequelizeModule内でSequelizeConfigServiceを作らず、別のモジュールからインポートされたプロバイダを使用するには、useExisting構文を使う。

SequelizeModule.forRootAsync({
  imports: [ConfigModule],
  useExisting: ConfigService,
});

SequelizeModuleは、新しいConfigServiceをインスタンス化するのではなく、既存のConfigServiceを再利用するために、インポートされたモジュールを検索する。

サンプル

実際に動作するサンプルはこちら