Skip to content

Transactions

Transactions ensure that a series of database operations either all succeed or all fail, maintaining data integrity. UQL provides several ways to handle transactions depending on your needs.

Perfect for NestJS and other Dependency Injection frameworks and/or where you have Services or Repositories. Use @Transactional() to wrap a method and @InjectQuerier() to access the managed connection as a parameter of the function. UQL automatically handles the entire lifecycle: acquiring/starting the transaction, committing on success, rolling back on error, and releasing the connection.

import { Transactional, InjectQuerier, type Querier } from '@uql/core';
import { User, Profile } from './shared/models/index.js';
export class UserService {
@Transactional()
async register(
userData: Partial<User>,
profileData: Partial<Profile>,
@InjectQuerier()
querier?: Querier
) {
const userId = await querier.insertOne(User, userData);
await querier.insertOne(Profile, { ...profileData, userId });
}
}

Section titled “2. Functional Transactions (Recommended for general cases)”

The functional approach is the most convenient way to run transactions manually. UQL automatically handles the entire lifecycle: acquiring/starting the transaction, committing on success, rolling back on error, and releasing the connection.

If you already have an active querier instance, you can use its transaction method to achieve automatic commit/rollback behavior.

import { pool } from './uql.config.js';
import { User, Profile } from './shared/models/index.js';
const querier = await pool.getQuerier();
try {
const result = await querier.transaction(async () => {
const userId = await querier.insertOne(User, { name: '...' });
await querier.insertOne(Profile, { userId, bio: '...' });
return userId;
});
} finally {
await querier.release();
}

This is an alternative way to run a group of operations atomically by obtaining a fresh connection from the pool.

import { pool } from './uql.config.js';
import { User, Profile } from './shared/models/index.js';
const result = await pool.transaction(async (querier) => {
const user = await querier.findOne(User, { $where: { email: '...' } });
const profileId = await querier.insertOne(Profile, { userId: user.id, bio: '...' });
return { userId: user.id, profileId };
});
// The querier is automatically released after the transaction

For scenarios requiring granular control over the transaction lifecycle, you can manually manage the transaction on a querier instance.

[!WARNING] When using manual transactions, always ensure queriers are released back to the pool, even in the event of an error.

import { pool } from './uql.config.js';
import { User, Profile } from './shared/models/index.js';
async function registerUser(userData: any, profileData: any) {
// 1. Obtain a querier from the pool
const querier = await pool.getQuerier();
try {
// 2. Start the transaction
await querier.beginTransaction();
const userId = await querier.insertOne(User, userData);
await querier.insertOne(Profile, { ...profileData, userId });
// 3. Commit the transaction
await querier.commitTransaction();
} catch (error) {
// 4. Rollback on error
await querier.rollbackTransaction();
throw error;
} finally {
// 5. Always release the querier
await querier.release();
}
}
ApproachLifecycle ManagementUse Case
@Transactional()AutomaticNestJS and DI frameworks.
querier.transaction()Semi-AutomaticWhen you already have a querier.
pool.transaction()AutomaticGeneral purpose standalone transactions.
beginTransaction()ManualExtreme control, low-level needs.