NestJS(TypeORM)でRepositoryクラスを跨いだトランザクション処理を実現する
こんにちは、今回は NestJS のトランザクション処理についての記事です。
実現したいこと
表題の通り、Reposiotry
クラス間を跨いだトランザクション処理を実現したいと思います。
NestJS の公式ドキュメントの方にも、トランザクション処理の方法についての記載はされているのですが、弊社で採用している Repository
パターンを利用したトランザクションについての記述がなかったため、あるユースケースの中で手続き的に呼ばれる複数の Reposioty
クラスに跨ったトランザクションの実装をしていこうと思います。
実装
今回は例として以下のユースケースを想定します。
- ユーザーが作成される
- 作成されたユーザーに紐づくプロフィールが作成される
このようなユースケースを想定して、一旦トランザクションを利用しない状態で実装します。
なお、前提としてUsecase
はService
に依存し、Service
はRepository
に依存するようにしています。
ユーザー作成用の Usecase クラス
import { Inject, Injectable } from "@nestjs/common";
import { UserService } from "./service/user.service";
import { UserProfileService } from "./service/user-profile.service";
@Injectable()
export class CreateUserUsecase {
constructor(
@Inject(UserService) private readonly userService: UserService,
@Inject(UserProfileService)
private readonly userProfileService: UserProfileService
) {}
async execute(): Promise<void> {
const user = await this.userService.create({ name: "ユーザー1" });
const userProfile = await this.userProfileService.create({
userId: user.id,
introduction: "はじめまして",
});
}
}
ユーザー作成用の Service クラス
import { Inject, Injectable } from "@nestjs/common";
import { UserRepository } from "./repository/user.repository";
import { User } from "./entity/user.entity";
export class UserService {
constructor(
@Inject(UserRepository) private readonly userRepository: UserRepository
) {}
async create(args: { name: string }): Promise<User> {
return this.userRepository.create(args);
}
}
ユーザー作成用の Repository クラス
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { User } from "./entity/user.entity";
@Injectable()
export class UserRepository {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>
) {}
async create(args: { name: string }): Promise<User> {
const user = new User();
user.name = args.name;
return this.userRepository.save(user);
}
}
実装としては非常に簡潔なもので、CreateUserUsecase.execute
が呼ばれると、UserService.create
によってユーザーが作成され、その後 UserProfileService.create
によってプロフィールが作成されるというものです。
なお、プロフィール作成用の Service
クラス、Repository
クラスはユーザー作成用のものと同様のため今回は割愛させていただきます。
このような実装の場合、仮にプロフィール作成時にエラーが発生した場合、作成されたユーザーだけが DB にコミットされて残り、関連先のプロフィールが存在しないという不整合な DB 状態となります。
トランザクションを利用した実装
では、上記の実装にトランザクションを張っていきます。
ユーザー作成用の Usecase クラス(修正)
import { Inject, Injectable } from "@nestjs/common";
+ import { InjectEntityManager } from '@nestjs/typeorm';
+ import { EntityManager } from 'typeorm';
+ import { DB_CONNECTION_NAME } from '../../common/typeorm/db-connection-name';
import { UserService } from "./service/user.service";
import { UserProfileService } from "./service/user-profile.service";
@Injectable()
export class CreateUserUsecase {
constructor(
+ @InjectEntityManager(DB_CONNECTION_NAME) private readonly manager: EntityManager,
@Inject(UserService) private readonly userService: UserService,
@Inject(UserProfileService)
private readonly userProfileService: UserProfileService
) {}
async execute(): Promise<void> {
+ await this.manager.transaction(async (manager) => {
const user = await this.userService.create(
{ name: "ユーザー1" },
+ manager
);
const userProfile = await this.userProfileService.create(
{ userId: user.id, introduction: "はじめまして" },
+ manager
);
+ });
}
}
ユーザー作成用の Service クラス(修正)
import { Inject, Injectable } from "@nestjs/common";
+ import { EntityManager } from 'typeorm';
import { UserRepository } from "./repository/user.repository";
import { User } from "./entity/user.entity";
export class UserService {
constructor(
@Inject(UserRepository) private readonly userRepository: UserRepository
) {}
async create(
args: { name: string },
+ manager?: EntityManager
): Promise<User> {
- return this.userRepository.create(args);
+ return this.userRepository.create(args, manager);
}
}
ユーザー作成用の Repository クラス(修正)
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
- import { Repository } from "typeorm";
+ import { EntityManager, Repository } from "typeorm";
import { User } from "./entity/user.entity";
@Injectable()
export class UserRepository {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>
) {}
async create(
args: { name: string },
+ manager?: EntityManager
): Promise<User> {
const user = new User();
user.name = args.name;
- return this.userRepository.create(args);
+ return manager
+ ? manager.getRepository(User).save(user)
+ : this.repository.save(user);
}
}
実装としては、以上になります。
簡単に説明すると、
-
各
Service
クラス、Repository
クラスでは、manager
というオプショナルな引数を追加します。 -
Usecase
クラスでは、@InjectEntityManager()
デコレータを利用してEntityManager
をインジェクトし、トランザクションを開始後、各Service
クラスにmanager
を渡します。 -
Repository
クラスでは、manager
が存在する場合はmanager
からRepository
を生成し、そうでない場合はインジェクトされたRepository
を利用するようにしています。
以上のような実装で、manager
を受け渡された DB 処理に関してはトランザクション処理の対象となり、もしトランザクション内で何らかのエラーが発生した場合は、manager
を介して実行した DB 処理はロールバックされる仕組みとなります。
実行結果
試しに強制的にエラーを発生させてみます。
async execute(): Promise<void> {
await this.manager.transaction(async (manager) => {
const user = await this.userService.create(
{ name: "ユーザー1" },
manager
);
const userProfile = await this.userProfileService.create(
{ userId: user.id, introduction: "はじめまして" },
manager
);
+ throw new Error("テスト");
});
}
query: ROLLBACK
INSERT INTO `users`(`id`, `name`) VALUES (DEFAULT, ?) -- PARAMETERS: ["ユーザー1"]
INSERT INTO `user_profiles`(`id`, `user_id`, `introduction`) VALUES (DEFAULT, ?, ?) -- PARAMETERS: [1, "はじめまして"]
ユーザー、プロフィールの作成がロールバックされていることが確認できます。
また、DB の方を確認しても、ユーザー、プロフィールの作成は行われていませんでしたので、期待通りの動作となっていそうです。
おまけ:コミット前のデータを参照する場合
トランザクション内で、実行した処理はそのトランザクションが完了するまではコミットされませんが、コミットされる前に、それらのデータを参照したいケースがあると思います。その場合も同様に manager
から取得できる Repository
を介して、コミット前のデータを取得することが可能です。
@Injectable()
export class UserRepository {
// 省略
async findOneByName(
args: { name: string },
manager?: EntityManager
): Promise<User> {
return manager
? manager.getRepository(User).findOne({ where: { name: args.name } })
: this.userRepository.findOne({ where: { name: args.name } });
}
}
まとめ
いかがでしたでしょうか。manager
を上位クラスから引き渡していかなければなりませんが、NestJS での Repository
クラス間を跨いだトランザクション処理を実現することができました。もしかするとより良い方法があるかもしれませんが、今回はここまでとさせていただきます。もし、より良い方法があれば、コメントいただけると幸いです。
弊社では、GraphQL や NestJS を利用した技術刷新にも積極的に取り組んでおります。これらの技術を用いた開発に興味がある方は、ぜひ弊社の採用ページをご覧ください。
スペースを簡単に貸し借りできるサービス「スペースマーケット」のエンジニアによる公式ブログです。 弊社採用技術スタックはこちら -> whatweuse.dev/company/spacemarket
Discussion