Managing Transactions with a Generic BaseService and TransactionService in a Nest.js App with TypeORM (Part 2)
In Part 1 of this series, we discussed the importance of using a generic base service for handling database queries in a Nest.js application with TypeORM. Now, in Part 2, we’ll explore how to manage transactions using TransactionService
alongside the BaseService
. We'll also see how to integrate these services to ensure data integrity in your application.
Transaction Management with TransactionService
The TransactionService
you provided handles transaction management using TypeORM's QueryRunner
. It offers methods for starting, committing, rolling back, and releasing transactions.
// transaction.service.ts
import { Injectable } from '@nestjs/common';
import { QueryRunner, EntityManager } from 'typeorm';
import { Observable, from, map } from 'rxjs';
@Injectable()
export class TransactionService {
private queryRunner: QueryRunner;
constructor(private manager: EntityManager) {}
// ... Transaction methods (start, commit, rollback, release)
startTransactionByQueryRunner(): Observable<QueryRunner> {
const queryRunner = this.manager.connection.createQueryRunner();
queryRunner.connect();
return defer(() => queryRunner.startTransaction()).pipe(
map(() => {
return queryRunner;
}),
);
}
commitTransactionByQueryRunner(queryRunner: QueryRunner): Observable<any> {
return from(queryRunner.commitTransaction()).pipe(map(() => true));
}
rollbackTransactionByQueryRunner(queryRunner: QueryRunner): Observable<any> {
return from(queryRunner.rollbackTransaction()).pipe(map(() => true));
}
releaseByQueryRunner(queryRunner: QueryRunner): Observable<any> {
return from(queryRunner.release()).pipe(map(() => true));
}
}
Now, let’s integrate them TransactionService
with the BaseService
to ensure that database operations are wrapped in transactions.
Integrating Transaction Management with BaseService
Incorporating transaction management into the BaseService
is straightforward. We'll modify a few methods to accept a QueryRunner
instance when necessary, ensuring that operations occur within the same transaction.
// BaseService.ts
import { DeepPartial, FindManyOptions, FindOneOptions, Repository, QueryRunner, UpdateResult, SelectQueryBuilder } from 'typeorm';
import { Observable, defer, from, map, switchMap } from 'rxjs';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
export abstract class BaseService<T> {
constructor(protected readonly repository: Repository<T>) {}
// ...
/**
* Save an entity within a transaction.
* @param entity
* @param queryRunner
* @returns
*/
saveInTransaction(entity: DeepPartial<T>, queryRunner: QueryRunner): Observable<T> {
return defer(() => queryRunner.manager.getRepository(this.repository.target).save(entity));
}
/**
* Update an entity within a transaction.
* @param id
* @param entity
* @param queryRunner
* @returns
*/
updateInTransaction(id: any, entity: QueryDeepPartialEntity<T>, queryRunner: QueryRunner): Observable<UpdateResult> {
return defer(() => queryRunner.manager.getRepository(this.repository.target).update(id, entity));
}
// ...
/**
* Remove an entity within a transaction.
* @param entity
* @param queryRunner
* @returns
*/
removeInTransaction(entity: T, queryRunner: QueryRunner): Observable<any> {
return defer(() => queryRunner.manager.getRepository(this.repository.target).remove(entity));
}
}
Using Transaction Management in TestService
With the modifications in place, we can now use the TransactionService
transaction-aware methods the TestService
for more complex database operations:
// TestService.ts
import { Injectable } from '@nestjs/common';
import { Test } from './entities/test.entity';
import { BaseService } from '../../services/base.service';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { TransactionService } from '../../services/transaction.service'; // Import TransactionService
@Injectable()
export class TestService extends BaseService<Test> {
constructor(
@InjectRepository(Test)
protected readonly repository: Repository<Test>,
private readonly transactionService: TransactionService, // Inject TransactionService
) {
super(repository);
}
// ...
async complexOperation() {
const queryRunner = this.transactionService.startTransactionByQueryRunner(); // Create a new transaction
try {
// Perform operations within the transaction
const input1: Partial<Test> = { type: 'test1' };
const input2: Partial<Test> = { type: 'test2' };
await this.saveInTransaction(input1, queryRunner);
await this.saveInTransaction(input2, queryRunner);
// Commit the transaction
await this.transactionService.commitTransactionByQueryRunner(queryRunner);
} catch (error) {
// Handle errors and rollback on failure
await this.transactionService.rollbackTransactionByQueryRunner(queryRunner);
throw error;
} finally {
// Release the transaction
await this.transactionService.releaseByQueryRunner(queryRunner);
}
}
}
or by RXJS
// TestService.ts
import { Injectable } from '@nestjs/common';
import { Test } from './entities/test.entity';
import { BaseService } from '../../services/base.service';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { TransactionService } from '../../services/transaction.service'; // Import TransactionService
@Injectable()
export class TestService extends BaseService<Test> {
constructor(
@InjectRepository(Test)
protected readonly repository: Repository<Test>,
private readonly transactionService: TransactionService, // Inject TransactionService
) {
super(repository);
}
test() {
return forkJoin({
queryRunner: this.transactionService.startTransactionByQueryRunner(),
}).pipe(
switchMap(({ queryRunner }) => {
return of(queryRunner).pipe(
switchMap(() => {
const input1: Partial<Test> = {
type: 'test1',
};
const input2: Partial<Test> = {
type: 'test2',
};
return forkJoin({
test1: this.testService.saveInTransaction(input1,queryRunner),
test2: this.testService.saveInTransaction(input2,queryRunner),
});
}),
switchMap(() => {
return this.transactionService.commitTransactionByQueryRunner(
queryRunner,
);
}),
catchError((error) => {
return this.transactionService
.rollbackTransactionByQueryRunner(queryRunner)
.pipe(
switchMap(() => {
throw new InternalServerErrorException(error);
}),
);
}),
finalize(() => {
return this.transactionService.releaseByQueryRunner(queryRunner);
}),
);
}),
);
}
}
Conclusion
In this two-part series, we’ve discussed the benefits of using a generic base service for database operations in a Nest.js application with TypeORM. Additionally, we’ve explored how to integrate transaction management using the TransactionService
alongside the BaseService
to ensure data consistency and integrity.
By following these best practices, your application becomes more maintainable, consistent, and robust, making it easier to manage complex database operations and transactions effectively.