πUsing Transactions
Node-Boot supports transactions trough a @Transactional()
decorator that benefits from the auto-configurations provided by the @node-boot/starter-persistence starter package.
Transactions propagation across the request chain is ensured by leveraging Node.js Async Hooks capabilities.
In order to use transactions, you have to have the persistence layer active and configured by installing the @node-boot/starter-persistence package and configuring it in order to use your preferred database:
PersistencyAfter persistence layer is configured and working as expected, you can start decorating controller or service functions as transactional.
Using Transactional Decorator
Every service method that needs to be transactional, need to use the
@Transactional()
decorator.The decorator can take an optional
propagation
as argument to define the propagation behaviour.The decorator can take an optional
isolationLevel
as argument to define the
...
import {Transactional} from "@node-boot/starter-persistence";
@Service()
export class UserService {
constructor(
private readonly logger: Logger,
private readonly userRepository: UserRepository,
) {
}
@Transactional() // Will open a transaction if one doesn't already exist
public async createUser(userData: CreateUserDto): Promise<User> {
this.logger.info(`Creating user ${userData.name}`);
const existingUser = await this.userRepository.findOneBy({
email: userData.email,
});
return optionalOf(existingUser)
.ifPresentThrow(() => new HttpError(409, `This email ${userData.email} already exists`))
.elseAsync(() => this.userRepository.save(userData));
}
}
You can also use DataSource
and EntityManager
objects together with repositories in transactions:
@Service()
export class PostService {
constructor(
readonly repository: PostRepository,
readonly dataSource: DataSource
){}
@Transactional() // Will open a transaction if one doesn't already exist
async createAndGetPost(id, message): Promise<Post> {
const post = this.repository.create({ id, message })
await this.repository.save(post)
return dataSource.createQueryBuilder(Post, 'p').where('id = :id', id).getOne();
}
}
Transaction Propagation
The following propagation options can be specified:
MANDATORY
- Support a current transaction, throw an exception if none exists.NESTED
- Execute within a nested transaction if a current transaction exists, behave likeREQUIRED
else.NEVER
- Execute non-transactionally, throw an exception if a transaction exists.NOT_SUPPORTED
- Execute non-transactionally, suspend the current transaction if one exists.REQUIRED
(default behaviour) - Support a current transaction, create a new one if none exists.REQUIRES_NEW
- Create a new transaction, and suspend the current transaction if one exists.SUPPORTS
- Support a current transaction, execute non-transactionally if none exists.
Isolation Levels
The following isolation level options can be specified:
READ_UNCOMMITTED
- A constant indicating that dirty reads, non-repeatable reads and phantom reads can occur.READ_COMMITTED
- A constant indicating that dirty reads are prevented; non-repeatable reads and phantom reads can occur.REPEATABLE_READ
- A constant indicating that dirty reads and non-repeatable reads are prevented; phantom reads can occur.SERIALIZABLE
= A constant indicating that dirty reads, non-repeatable reads and phantom reads are prevented.
Transaction Hooks
Because you hand over control of the transaction creation to this library, there is no way for you to know whether or not the current transaction was successfully persisted to the database.
To circumvent that, we expose three helper methods that allow you to hook into the transaction lifecycle and take appropriate action after a commit/rollback.
runOnTransactionCommit(cb)
takes a callback to be executed after the current transaction was successfully committed
@Transactional()
async createPost(id, message): Promise<Post> {
const post = this.repository.create({ id, message });
const result = await this.repository.save(post);
runOnTransactionCommit(() => this.events.emit('post created'));
return result;
}
runOnTransactionRollback(cb)
takes a callback to be executed after the current transaction rolls back. The callback gets the error that initiated the rollback as a parameter.
@Transactional()
async createPost(id, message): Promise<Post> {
const post = this.repository.create({ id, message });
const result = await this.repository.save(post);
runOnTransactionRollback((e) => this.events.emit(e));
return result;
}
runOnTransactionComplete(cb)
takes a callback to be executed at the completion of the current transactional context. If there was an error, it gets passed as an argument.
@Transactional()
async createPost(id, message): Promise<Post> {
const post = this.repository.create({ id, message });
const result = await this.repository.save(post);
runOnTransactionComplete((e) => this.events.emit(e ? e : 'post created'));
return result;
}
Transactions API
Transactions Configuration
{
storageDriver?: StorageDriver,
maxHookHandlers?: number
}
storageDriver
- Determines which underlying mechanism (like Async Local Storage or cls-hooked) the library should use for handling and propagating transactions. By default, it'sStorageDriver.AUTO
.maxHookHandlers
- Controls how many hooks (commit
,rollback
,complete
) can be used simultaneously. If you exceed the number of hooks of same type, you get a warning. This is a useful to find possible memory leaks. You can set this options to0
orInfinity
to indicate an unlimited number of listeners. By default, it's10
.
node-boot:
persistence:
...
transactions:
maxHookHandlers: 10
storageDriver: "AUTO";
@Transactional Options
{
isolationLevel?: IsolationLevel;
propagation?: Propagation;
}
isolationLevel
- isolation level for transactional context (isolation levels)propagation
- propagation behaviour's for nest transactional contexts (propagation behaviors)
Storage Driver
Option that determines which underlying mechanism the library should use for handling and propagating transactions.
The possible variants:
AUTO
- Automatically selects the appropriate storage mechanism based on the Node.js version, usingAsyncLocalStorage
for Node.js versions 16 and above, and defaulting tocls-hooked
for earlier versions.CLS_HOOKED
- Utilizes thecls-hooked
package to provide context storage, supporting both legacy Node.js versions with AsyncWrap for versions below 8.2.1, and usingasync_hooks
for later versions.ASYNC_LOCAL_STORAGE
- Uses the built-inAsyncLocalStorage
feature, available from Node.js version 16 onwards,
Utility Functions
Just in case you are not a fun of decorators, you can make use of the following utility functions to handle transactions:
runInTransaction(fn: Callback, options?: Options): Promise<...>
Run code in transactional context.
...
runInTransaction(() => {
...
const user = this.usersRepo.update({ id: 1000 }, { state: action });
...
}, { propagation: Propagation.REQUIRES_NEW });
...
wrapInTransaction(fn: Callback, options?: Options): WrappedFunction
Wrap function in transactional context
...
const updateUser = wrapInTransaction(() => {
...
const user = this.usersRepo.update({ id: 1000 }, { state: action });
...
}, { propagation: Propagation.NEVER });
...
await updateUser();
...
Last updated