Appearance
Transactions
WARNING
This is our own implementation of transactions! For this reason you should always import it with @wisemen/nestjs-typeorm. Default repositories must be injected with InjectRepository.
Custom repositories must extend from TypeOrmRepository.
To ensure data integrity, it is sometimes necessary to group multiple database operations into a single transaction. When one of the operations fails, the entire transaction will be rolled back. This prevents partial updates and maintains data integrity.
Why Transactions?
If your logic updates multiple entities, a transaction ensures consistency. Without it, a failure mid-operation might leave the database in an inconsistent state.
Key Points:
- Avoid keeping transactions open longer than necessary.
- Avoid other asynchronous operations within the transaction callback. (e.g., API calls, file operations, ...)
- Only include logic within the transaction that absolutely requires rollback support.
When to Avoid Transactions:
- For simple fetch or insert operations.
- When rollback is not a concern.
Using Transactions
To group multiple operations into a single transaction you can use the transaction() helper function. This uses AsyncLocalStorage to make sure every query run in this scope will use the transaction.
typescript
import { InjectRepository, transaction } from "@wisemen/nestjs-typeorm";
export class ActivateUserUseCase {
constructor(
private readonly dataSource: DataSource,
@InjectRepository(User)
private userRepository: Repository<User>,
@InjectRepository(AnotherEntity)
private anotherEntityRepository: Repository<AnotherEntity>
) {}
async execute(input: string): Promise<void> {
await transaction(this.dataSource, async () => {
await this.userRepository.update(
{
email: input,
},
{
isActive: true,
}
);
await this.anotherEntityRepository.update(
{
email: input,
},
{
isActive: true,
}
);
});
}
}This transaction function executes the given callback within a transaction. When the given callback function completes successfully, the transaction is committed. If an error occurs within the callback, the transaction is rolled back.
If for some reason, you need to use the entity manager which holds the transaction, you can access it as the first argument of the callback function.
Readonly Contexts (Read Replicas)
For high-read workloads, use the readonly() helper to route queries to read replicas. Readonly and transaction scopes are mutually exclusive.
See the dedicated page: Readonly for details, usage patterns, and caveats.
Mutual Exclusivity
Readonly and transaction scopes are mutually exclusive to prevent mixing primary and replica contexts:
- Starting a transaction inside a readonly scope throws
Error('Cannot start a transaction inside a readonly context'). - Starting a readonly scope inside a transaction throws
Error('Cannot start a readonly context inside a transaction'). - Calling
EntityManager.transaction(...)while in a readonly scope throwsError('Cannot call EntityManager.transaction inside a readonly context').
Automatic Routing via Proxies
Repositories and services receive a proxied EntityManager so you almost never need to pass managers around manually:
- The manager is wrapped so that on each access it resolves to the correct context.
- Precedence: in a transaction → use transaction manager; else if in readonly → use readonly manager; else → use default manager.
- This composition is used throughout repositories and schedulers so routing is automatic.
Troubleshooting
If transactions do not seem to be applied, ensure your services and repositories are using the proxied manager provided by @wisemen/nestjs-typeorm and that you’re not mixing readonly and transaction scopes.
Checkpoints
Executing multiple nested transactions makes a checkpoint for each call within the first transaction.
