nestjs-clsライブラリを使うことで、REQUEST_SCOPEのパフォーマンス問題を解消しつつ、リクエストスコープでのトランザクション管理を実現できる。
REQUEST_SCOPEの問題
- リクエストごとにDIサブツリー全体を再作成
- 大量のProviderインスタンス生成によるメモリ・CPU負荷
- 特に大規模なNestJSアプリケーションで顕著
nestjs-cls による解決策
Continuation-Local Storage (CLS) でリクエストコンテキストを管理し、Providerは全てSingletonのまま維持。
// インストール
npm install nestjs-cls
// モジュール設定
ClsModule.forRoot({
global: true,
middleware: { mount: true },
})
// Proxy Providerで動的にDB接続を解決
ClsModule.forFeatureAsync({
provide: DB_CLIENT,
inject: [DATABASE_POOL, ClsService],
useFactory: (pool: Pool, cls: ClsService) => {
// トランザクション中ならトランザクション用を返す
const txDb = cls.get('transactionDb');
if (txDb) return txDb;
// 通常はプール接続を返す
return drizzle(pool, { schema });
},
})
トランザクションヘルパー
@Injectable()
export class TransactionService {
constructor(
@Inject(DATABASE_POOL) private pool: Pool,
private cls: ClsService,
) {}
async run<T>(callback: () => Promise<T>): Promise<T> {
// ネスト対応: 既にトランザクション中ならそのまま実行
if (this.cls.get('inTransaction')) {
return callback();
}
const connection = await this.pool.getConnection();
const txDb = drizzle(connection, { schema });
this.cls.set('transactionDb', txDb);
this.cls.set('inTransaction', true);
try {
await connection.beginTransaction();
const result = await callback();
await connection.commit();
return result;
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
this.cls.set('transactionDb', null);
this.cls.set('inTransaction', false);
}
}
}
使用例
// Service
async createWithRelations(data) {
return this.transactionService.run(async () => {
// this.db は自動的にトランザクション用DBを使う
const parent = await this.db.insert(parents).values(data);
// 別のServiceを呼んでも同じトランザクション
await this.childService.create({ parentId: parent.id });
return parent;
});
}
利点
- Singletonのまま、リクエストコンテキスト分離
- パフォーマンス大幅改善 (DI再作成なし)
- 複数Serviceで自動的にトランザクション共有
- ネストしたトランザクション呼び出しに対応
- Spring FrameworkのようなDI体験
注意点
- Batch/Workerでは手動でCLSコンテキスト作成が必要
- テストでのCLS設定が必要