This repository shows how to implement unit of work pattern in NestJs Framework to realize transactional work with TypeORM.
@Post()
async create(@Body() orderDto: CreateOrderRequestDto) {
return this.unitOfWork.doTransactional(async (): Promise<Order> => {
return this.orderService.createOrder(orderDto); <-- running in a transactional operation
});
}
The database.sql
file has the database structure necessary to run this example.
The configuration of database connection is set in src/app.module.ts
.
Installing the dependencies:
npm install
Running the application:
npm run start:dev
NestJs it's an amazing frameworks to NodeJs, with a powerful Dependency Injection Service, BUT when we need to work with TypeORM Repositories in transaction operation, the beautiful of repository injection fade away.
See below some issues about it:
- ISSUE: Better transaction management
- ISSUE: Transaction management in service layer
- ISSUE: support Distributed Transaction Service , like spring JTA
- ISSUE: Transactions in NestJs
- ISSUE: Transactions support
There are other repositories/libs that try to resolve the same problem:
My implementation uses a service class called UnitOfWorkService to share the same connection between the custom repositories.
@Injectable({
scope: Scope.REQUEST, // <-- VERY IMPORTANT to create one instance per request
})
export class UnitOfWorkService {
constructor(
@InjectConnection()
private readonly connection: Connection, // <-- get the database connection
) {
this.manager = this.connection.manager; // <-- set the default manager
}
private manager: EntityManager;
getManager() {
return this.manager;
}
async doTransactional<T>(fn): Promise<T> {
return await this.connection.transaction(async (manager) => {
this.manager = manager; // <-- set the entity manager to share
return fn(manager); // <-- executes the transactional work
});
}
}
My approach doesn't work with default TypeORM repositories provided by injection, you need to implement your own repository or generate it from TypeORM EntityManager shared.
import { Injectable } from '@nestjs/common';
import { Order } from '../models/order.model';
import { UnitOfWorkService } from '../../core/services/unit-of-work.service';
import { Item } from '../models/item.model';
@Injectable()
export class OrderRepository {
constructor(private readonly uow: UnitOfWorkService) {} // <-- receive the UnitOfWorkService with the manager
async getAll(): Promise<Order[]> {
return this.uow.getManager().find(Order, {
relations: ['items'],
});
}
async getById(idOrder: number): Promise<Order> {
return this.uow.getManager().findOneOrFail(Order, idOrder, {
relations: ['items'],
});
}
async saveOrder(order: Order): Promise<Order> {
return this.uow.getManager().save(order);
}
async saveOrderItem(item: Item): Promise<Item> {
return this.uow.getManager().save(item);
}
}
The entity service must only receive the custom repository by injection:
import { Injectable } from '@nestjs/common';
import { OrderRepository } from '../repositories/order.repository';
import { CreateOrderRequestDto } from '../dto/create-order-request.dto';
import { Order } from '../models/order.model';
import { Item } from '../models/item.model';
@Injectable()
export class OrderService {
constructor(private readonly orderRepository: OrderRepository) {} // <-- the custom repo created before
async getAll(): Promise<Order[]> {
return this.orderRepository.getAll();
}
async getById(id: number): Promise<Order> {
return this.orderRepository.getById(id);
}
async createOrder(orderDto: CreateOrderRequestDto): Promise<Order> {
const order = new Order();
order.date = orderDto.date;
order.description = orderDto.description;
await this.orderRepository.saveOrder(order);
for (const itemDto of orderDto.items) {
const item = new Item();
item.name = itemDto.name;
item.quantity = itemDto.quantity;
item.order = order;
await this.orderRepository.saveOrderItem(item);
}
return order;
}
}
The controller needs only receive the UnitOfWorkService by injection:
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { OrderService } from '../services/order.service';
import { Order } from '../models/order.model';
import { CreateOrderRequestDto } from '../dto/create-order-request.dto';
import { UnitOfWorkService } from '../../core/services/unit-of-work.service';
@Controller('/v1/order')
export class OrderController {
constructor(
private readonly unitOfWork: UnitOfWorkService, // <-- inject the UOW to handle transaction operation
private readonly orderService: OrderService,
) {}
@Get()
async all(): Promise<Order[]> {
return this.orderService.getAll(); // <-- use the service with the default (non-transactional) manager
}
@Get()
async getById(@Param('id') id: number): Promise<Order> {
return this.orderService.getById(id); // <-- use the service with the default (non-transactional) manager
}
@Post()
async create(@Body() orderDto: CreateOrderRequestDto) {
return this.unitOfWork.doTransactional(async (): Promise<Order> => { // <-- start a transaction operation
return this.orderService.createOrder(orderDto);
});
}
}
Contributions are always welcome!
I will be glad to know if this approach help you or if you know a better way to resolve the same problem.