データベースとの連携(TypeORM)

データベース

NestJSはデータベースに依存しないので、あらゆるSQLまたはNoSQLデータベースと簡単に結合できます。あなたの好みに応じて、多くのオプションが用意されています。
もっとも一般的なレベルとしては、NestJSをデータベースに接続するにはExpressやFastifyと同じように、データベース用の適切なNode.jsドライバをロードする必要があります。
また、より高い抽象度で操作するために、MikroORM、Knex.js、TypeORM、Prismaなどの汎用的Node.jsデータベース統合ライブラリやORMを直接使用できます。
利便性のために、NestJSはTypeORMとSequelizeとの緊密な統合を、それぞれ@nestjs/typeorm@nestjs/sequelizeパッケージでアウトオブボックスで提供しています。これらの統合は、モデル/リポジトリインジェクション、テスタビリティ、非同期設定などNestJS特有の機能を追加し、選択したデータベースへのアクセスをより簡単にします。

TypeORM

SQLやNoSQLのデータベースと統合するために、NestJSでは@nestjs/typeormパッケージを提供しています。TypeORMはTypeScriptで活用できるもっとも成熟したORMであるため、NestJSではTypeORMをデフォルトとして使っています。
TypeORMはTypeScriptで書かれているので、NestJSのフレームワークと上手に統合されています。
使い始めるために、まず必要な依存関係をインストールしておきます。この章では、人気のあるMySQLを使ったデモを行いますが、TypeORMは他にも以下のようなデータベースをサポートしています。
  • PostgreSQL
  • Oracle
  • Microsoft SQL Server
  • SQLite
  • MongoDB
本セクションで説明する手順は、TypeORMがサポートするどのデータベースでも同じです。選択したデータベースに関連するクライアントAPIライブラリをインストールするだけでいいです。
npm install --save @nestjs/typeorm typeorm mysql2
インストールが完了したら、ルートのAppModuleTypeOrmModuleをインポートします。
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 {}
注意
synchronize: trueの設定は、本番環境では使用しないでください。そうしないと、本番データを失う可能性が生じます。
forRoot()メソッドは、TypeORMパッケージのDataSourceコンストラクタによって公開されるすべての構成プロパティをサポートしています。
加えて、次に説明するいくつかの追加設定プロパティがあります。
NestJSはデータベースに依存しないので、あらゆるSQLまたはNoSQLデータベースと簡単に結合できます。あなたの好みに応じて、多くのオプションが用意されています。
もっとも一般的なレベルでは、NestJSをデータベースに接続するにはExpressあるいはFastifyと同じようにデータベース用の適切なNode.jsドライバをロードしておく必要があります。

TypeORMとの結合

SQLやNoSQLのデータベースと連携するために、NestJSでは@nestjs/typeormパッケージを提供しています。
TypeORMはTypeScriptで利用できるもっとも成熟したORMです。NestJSではTypeORMをデフォルトで活用しています。TypeORMはTypeScriptで書かれているので、NestJSとの相性は抜群です。
使い始めるために、まずは必要な依存関係をインストールします。この章では、人気のあるMySQLを使ったデモを行います。それ以外にもNestJSは以下のようなデータベースをサポートしています。
  • PostgreSQL
  • Oracle
  • Microsoft SQL Server
  • SQLite
  • MongoDB
本セクションで説明する手順は、TypeORMがサポートするどのデータベースでも同じです。選択したデータベースに関連するクライアントAPIライブラリをインストールするだけでいいです。
npm install --save @nestjs/typeorm typeorm mysql2
一旦インストールが完了したら、AppModuleTypeOrmModuleをインポートします。
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 {}
注意
synchronize: trueの設定は、本番環境では使用しないでください。そうしないと本番環境でデータを失う可能性があります
forRoot()メソッドは、TypeORMパッケージのDataSourceコンストラクタによって公開される全ての構成プロパティをサポートしています。加えて、以下に説明するいくつかの追加設定プロパティがあります。

retryAttempts

データベースへの接続試行回数。デフォルト:10

retryDelay

接続再試行間隔(ms)。デフォルト:3000

autoLoadEntities

trueの場合、エンティティは自動的にロードされます。デフォルト:false
終了したら、TypeORMのDataSourceEntityManagerオブジェクトは、例えば(モジュールをインポートする必要なく)プロジェクト全体でインジェクトできるようになります。
app.module.ts
import { DataSource } from 'typeorm';
@Module({
imports: [TypeOrmModule.forRoot(), UsersModule],
})
export class AppModule {
constructor(private dataSource: DataSource) {}
}

リポジトリパターン

TypeORMはリポジトリデザインパターンをサポートしているので、各エンティティは独自のリポジトリを持ちます。これらのリポジトリは、データベースのデータソースから取得できます。
この例を実装するには、少なくとも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;
}
メモ
エンティティに関して詳細に知りたければ、TypeORM公式ドキュメントを参照してください。
Userエンティティファイルは、usersディレクトリに格納されています。このディレクトリにはUsersModuleに関連するすべてのファイルが含まれています。モデルファイルをどこに置くかは自由ですが、ドメインの近くで、対応するモジュールディレクトリに作成することを推奨します。
Userエンティティの使用を開始するには、モジュールforRoot()メソッドのオプションにあるentities配列に挿入し、TypeORMに知らせる必要があります。
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);
}
}
メモ
AppModuleUsersModuleをインポートすることを忘れないようにしてください。
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)が使えるようになります。
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つのTypeがあります。
One-to-one
主テーブルのすべての行は、従属テーブルの行とひとつだけ関連づけられます。この種のリレーションを定義するには、@OneToOne()デコレータを使用します。
One-to-many/Many-to-one
主テーブルのすべての行は、外部テーブルのひとつ以上の関連行を持ちます。この種のリレーションを定義するには、 @OneToMany()および @ManyToOne()デコレータを使用します。
Many-to-Many
主テーブルのすべての行は、外部テーブルの多くの関連行を持ち、 外部テーブルのすべてのレコードは、主テーブルの多くの関連行を持ちます。このタイプのリレーションを定義するには、 @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[];
}
メモ
TypeORMのリレーションについて詳細に学びたければ、TypeORMの公式ドキュメントを参照してください。

エンティティの自動ロード

データソースオプションのエンティティ配列にエンティティを手動で追加するのは面倒です。また、ルートモジュールからエンティティを参照すると、アプリケーションのドメイン境界が壊れ、アプリケーションの他の部分に実装の詳細が漏れる原因になります。この問題に対処するために、別の解決策が提供されています。エンティティを自動的にロードするには、以下のように、構成オブジェクト(forRoot()メソッドに渡される)のautoLoadEntitiesプロパティをtrueに設定します。
app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
...
autoLoadEntities: true,
}),
],
})
export class AppModule {}
このオプションを設定すると、forFeature()メソッドで登録されたすべてのエンティティが設定オブジェクトのエンティティ配列に自動で追加される。
注意
forFeature()メソッドで登録されていない、エンティティから参照されているだけのエンティティは、autoLoadEntitiesの設定で含まれないことに注意してください。

エンティティ定義を分離する

デコレータを使えば、エンティティやそのカラムをモデル内で直接定義できます。しかし、「エンティティスキーマ」を使って、エンティティやカラムを別のファイルに定義することを好む人もいます。
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
},
},
});
注意
targetオプションを指定した場合、nameオプションの値はターゲットクラスの名前と同じでなければいけません。targetを指定しない場合は、任意の名前を使えます。
NestJSでは、Entityが期待される場所ならどこでもEntitySchemaインスタンスを使えます。
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クラスを使用すると、トランザクションを完全に制御することができるため、使用をお勧めします。
まず、通常の方法でDataSourceオブジェクトをクラスにインジェクトする必要があります。
@Injectable()
export class UsersService {
constructor(private dataSource: DataSource) {}
}
メモ
DataSourceクラスはtypeormパッケージからインポートできます。
そして、このオブジェクトを使ってトランザクションを作成できます。
async createMany(users: User[]) {
const queryRunner = this.dataSource.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) {
// since we have errors lets rollback the changes we made
await queryRunner.rollbackTransaction();
} finally {
// you need to release a queryRunner which was manually instantiated
await queryRunner.release();
}
}
メモ
dataSourceQueryRunnerを作成するためだけに使用されていることに注意してください。しかし、このクラスをテストするにはDataSourceオブジェクト全体 (いくつかのメソッドを公開しています) をモックする必要があります。そこで、ヘルパーファクトリークラス (例: QueryRunnerFactory) を使用し、トランザクションを維持するために必要なメソッドを限定したインターフェイスを定義することをお勧めします。この手法により、これらのメソッドのモックが非常に簡単になります。
また、DataSourceオブジェクトのTransactionメソッドを使ったコールバックスタイルのアプローチもできます。
async createMany(users: User[]) {
await this.dataSource.transaction(async manager => {
await manager.save(users[0]);
await manager.save(users[1]);
});
}
トランザクションを制御するデコレータ(@Transaction()および@TransactionManager())を使うのは推奨されません。

SubScriber

TypeORMのSubscriberを活用すると、特定のエンティティイベントをリッスンできます。
import {
DataSource,
EntitySubscriberInterface,
EventSubscriber,
InsertEvent,
} from 'typeorm';
import { User } from './user.entity';
@EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<User> {
constructor(dataSource: DataSource) {
dataSource.subscribers.push(this);
}
listenTo() {
return User;
}
beforeInsert(event: InsertEvent<User>) {
console.log(`BEFORE USER INSERTED: `, event.entity);
}
}
ここで、UserSubscriberクラスをproviders配列に追加します。
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 {}
メモ
Subscribersについて詳しく学びたければこちらを参照ください。

マイグレーション

マイグレーションとは、データベース内の既存のデータを保存しつつ、アプリケーションのデータモデルと同期させるために、データベースのスキーマをインクリメンタル(段階的、漸進的)に更新する方法を提供します。マイグレーションの生成、実行や復帰を行うためにTyoeORMは専用のCLIを提供しています。
マイグレーションクラスはNestアプリケーションのソースコードから分離されています。そのライフサイクルはTypeORM CLIによって管理されます。したがって、依存性注入やその他NestJSの特有機能をマイグレーションで使うことはできません。マイグレーションの詳細については、TypeORMの公式ドキュメントを参照してください。

複数のデータベースへの接続

プロジェクトによっては、複数のデータベース接続が必要な場合があります。これもこのModuleで実現できます。複数の接続を扱うには、まず接続を作成します。この場合、データソースの命名が必要になります。
独自のデータベースに保存されたアルバムエンティティがあるとします。
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 {}
注意
データソースにnameを設定しない場合、その名前はdefaultで設定されます。名前のない接続、または同じ名前の接続を複数持つことはできませんので注意してください。
もしTypeOrmModule.forRootAsyncを使いたいなら、useFactoryの外部にデータソースを設置する必要があります。例えばこのようにします。
TypeOrmModule.forRootAsync({
name: 'albumsConnection',
useFactory: ...,
inject: ...,
}),
詳細はGitHub Issueをご確認ください。
この時点で、UserAlbumのエンティティがそれぞれのデータソースに登録されています。この状態で、TypeOrmModule.forFeature()メソッドと@InjectRepository()デコレータに、どのようなリソースを使用するかを指示する必要があります。
もし、データソース名を渡さなかった場合は、デフォルトのデータソースが使用されます。
@Module({
imports: [
TypeOrmModule.forFeature([User]),
TypeOrmModule.forFeature([Album], 'albumsConnection'),
],
})
export class AppModule {}
また指定したデータソースのDataSourceEntityManagerをインジェクトできます。
@Injectable()
export class AlbumsService {
constructor(
@InjectConnection('albumsConnection')
private dataSource: DataSource,
@InjectEntityManager('albumsConnection')
private entityManager: EntityManager,
) {}
}
また、任意のDataSourceをプロバイダにインジェクトできます。
@Module({
providers: [
{
provide: AlbumsService,
useFactory: (albumsConnection: DataSource) => {
return new AlbumsService(albumsConnection);
},
inject: [getDataSourceToken('albumsConnection')],
},
],
})
export class AlbumsModule {}

テスト

アプリへのユニットテストを行う場合、通常はデータベースへの接続を避けて、テストスイートを独立させて、その実行プロセスを可能な限り高速にしたいと考えるでしょう。しかし、私たちのクラスはデータソース(接続)インスタンスから引き出されるリポジトリに依存するかもしれません。
その場合は、モックリポジトリを作成しましょう。これを実現するために、カスタムプロバイダを設定します。登録された各リポジトリは、自動的に<EntityName>Repositoryトークンで表現されます。EntityNameはエンティティクラスの名前です。
@nestjs/typeormパッケージは、指定されたエンティティに基づいたプリペアトークンを返すgetRepositoryToken関数を公開しています。
@Module({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useValue: mockRepository,
},
],
})
export class UsersModule {}
これで、UsersRepositoryとして代用されるmockRepositoryが使用されるようになります。クラスが @InjectRepository()デコレーターを使ってUsersRepositoryを要求した場合、Nestは登録されたmockRepositoryオブジェクトを使用します。

非同期通信

リポジトリモジュールのオプションを静的に渡すのではなく、非同期に渡したい場合があります。この場合は forRootAsync()メソッドを使用します。このメソッドは、非同期な設定を扱うためのいくつかの方法を提供しています。
ひとつの方法として、ファクトリー関数を使用する方法があります。
TypeOrmModule.forRootAsync({
useFactory: () => ({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [],
synchronize: true,
}),
});
私たちのファクトリーは他の非同期プロバイダーのように振る舞います。(例えば、asyncに設定でき、injectによって依存関係を注入することができます)
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'mysql',
host: configService.get('HOST'),
port: +configService.get('PORT'),
username: configService.get('USERNAME'),
password: configService.get('PASSWORD'),
database: configService.get('DATABASE'),
entities: [],
synchronize: true,
}),
inject: [ConfigService],
});
また、useClass構文も使うことができます。
TypeOrmModule.forRootAsync({
useClass: TypeOrmConfigService,
});
上記のコンストラクションはTypeOrmModule内でTypeOrmConfigServiceをインスタンス化し、それを使ってcreateTypeOrmOptions()を呼び出すことでオプションオブジェクトを提供します。これは、TypeOrmConfigServiceが以下に示すようにTypeOrmOptionsFactoryインタフェースを実装しなければならないことを意味することに注意しましょう。
@Injectable()
class TypeOrmConfigService implements TypeOrmOptionsFactory {
createTypeOrmOptions(): TypeOrmModuleOptions {
return {
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [],
synchronize: true,
};
}
}
TypeOrmModule内部でTypeOrmConfigServiceが生成されないようにします。そして、別のモジュールからインポートしたプロバイダを使用するために、useExisting構文を使えます。
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useExisting: ConfigService,
});
TypeOrmModuleはインポートされたModuleを検索し、新しいConfigServiceをインスタンス化するかわりに既存のConfigServiceを再利用します。
メモ
nameプロパティが、useFactoryuseClass、またはuseValueプロパティと同じレベルで定義されていることを確認してください。これにより、Nestは適切なインジェクショントークンの下でデータソースを適切に登録することができます。

DataSourceファクトリーのカスタマイズ

useFactoryuseClassuseExistingを用いた非同期設定と合わせて、オプションでdataSourceFactory関数を指定することで、TypeOrmModuleにデータソースを作成させるのではなく、独自のTypeORMデータソースを提供させることができます。
dataSourceFactoryuseFactoryuseClassuseExistingを用いた非同期設定時に設定されたTypeORM DataSourceOptionsを受け取り、TypeORM DataSourceを解決するPromiseを返します。
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
// Use useFactory, useClass, or useExisting
// to configure the DataSourceOptions.
useFactory: (configService: ConfigService) => ({
type: 'mysql',
host: configService.get('HOST'),
port: +configService.get('PORT'),
username: configService.get('USERNAME'),
password: configService.get('PASSWORD'),
database: configService.get('DATABASE'),
entities: [],
synchronize: true,
}),
// dataSource receives the configured DataSourceOptions
// and returns a Promise<DataSource>.
dataSourceFactory: async (options) => {
const dataSource = await new DataSource(options).initialize();
return dataSource;
},
});
メモ
DataSourceクラスはtypeormパッケージからインポートします。