データベースとの連携(TypeORM)
NestJSはデータベースに依存しないので、あらゆるSQLまたはNoSQLデータベースと簡単に結合できます。あなたの好みに応じて、多くのオプションが用意されています。
利便性のために、NestJSはTypeORMとSequelizeとの緊密な統合を、それぞれ
@nestjs/typeorm
と@nestjs/sequelize
パッケージでアウトオブボックスで提供しています。これらの統合は、モデル/リポジトリインジェクション、テスタビリティ、非同期設定などNestJS特有の機能を追加し、選択したデータベースへのアクセスをより簡単にします。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
インストールが完了したら、ルートの
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 {}
注意
synchronize: true
の設定は、本番環境では使用しないでください。そうしないと、本番データを失う可能性が生じます。forRoot()
メソッドは、TypeORMパッケージのDataSource
コンストラクタによって公開されるすべての構成プロパティをサポートしています。加えて、次に説明するいくつかの追加設定プロパティがあります。
NestJSはデータベースに依存しないので、あらゆるSQLまたはNoSQLデータベースと簡単に結合できます。あなたの好みに応じて、多くのオプションが用意されてい ます。
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
一旦インストールが完了したら、
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 {}
注意
synchronize: true
の設定は、本番環境では使用しないでください。そうしないと本番環境でデータを失う可能性がありますforRoot()
メソッドは、TypeORMパッケージのDataSource
コンストラクタによって公開される全ての構成プロパティをサポートしています。加えて、以下に説明するいくつかの追加設定プロパティがあります。データベースへの接続試行回数。デフォルト:
10
接続再試行間隔(ms)。デフォルト:
3000
true
の場合、エンティティは自動的にロードされます。デフォルト:false
終了したら、TypeORMの
DataSource
とEntityManager
オブジェクトは、例えば(モジュールをインポートする必要なく)プロジェクト全体でインジェクトできるようになります。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;
}
メモ
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()
デコレーターを使用して UsersRepository
をUsersService
にインジェクトします。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);
}
}
メモ
AppModule
にUsersModule
をインポートすることを忘れないようにしてください。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 {}
ここで、
UserHttpModule
でUsersModule
をインポートすると、後者のモジュールのプロバイダで@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[];
}
メモ
データソースオプションのエンティティ配列にエンティティを手動で追加するのは面倒です。また、ルートモジュールからエンティティを参照すると、アプリケーションのドメイン境界が壊れ、アプリケーションの他の部分に実装の詳細が漏れる原因になります。この問題に対処するために、別の解決策が提供されています。エンティティを自動的にロードするには、以下のように、構成オブジェクト(
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 {}
データベーストランザクションは、データベース管理システム内でデータベースに対して行われる作業の単位を表し、他のトランザクションから独立した首 尾一貫した信頼できる方法で処理されます。トランザクションは一般に、データベースのあらゆる変更を意味します。(詳細はこちら)
まず、通 常の方法で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();
}
}
メモ
dataSource
はQueryRunner
を作成するためだけに使用されていることに注意してください。しかし、このクラスをテストするには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()
)を使うのは推奨されません。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 {}
メモ
マイグレーションとは、データベース内の既存のデータを保存しつつ、アプリケーションのデータモデルと同期させるために、データベースのスキーマをインクリメンタル(段階的、漸進的)に更新する方法を提供します。マイグレーションの生成、実行や復帰を行うために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: ...,
}),
この時点で、
User
とAlbum
のエンティティがそれぞれのデータソースに登録されています。この状態で、TypeOrmModule.forFeature()
メソッドと@InjectRepository()
デコレータに、どのようなリソースを使用するかを指示する必要があります。もし、データソース名を渡さなかった場合は、デフォルトのデータソースが使用されます。
@Module({
imports: [
TypeOrmModule.forFeature([User]),
TypeOrmModule.forFeature([Album], 'albumsConnection'),
],
})
export class AppModule {}
また指定したデータソースの
DataSource
やEntityManager
をインジェクトできます。@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
プロパティが、useFactory
、useClass
、またはuseValue
プロパティと同じレベルで定義されていることを確認してください。これにより、Nestは適切なインジェクショントークンの下でデータソースを適切に登録することができます。useFactory
、useClass
、useExisting
を用いた非同期設定と合わせて、オプションでdataSourceFactory
関数を指定することで、TypeOrmModule
にデータソースを作成させるのではなく、独自のTypeORMデータソースを提供させることができます。dataSourceFactory
はuseFactory
、useClass
、useExisting
を用いた非同期設定時に設定された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
パッケージからインポートします。Last modified 11mo ago