Transactional API usage in LoopBack 4
Overview
A transaction is a sequence of data operations performed as a single logicalunit of work. Many relational databases support transactions to help enforcedata consistency and business logic requirements.
A repository can perform operations in a transaction when the backing datasourceis attached to one of the following connectors:
- MySQL connector (IMPORTANT: Only with InnoDB as thestorage engine).
- PostgreSQL connector
- SQL Server connector
- Oracle connector
- DB2 Connector
- DashDB Connector
- DB2 iSeries Connector
- DB2 for z/OS connector
- Informix connectorThe repository class needs to extend from
TransactionalRepository
repositoryinterface which exposes thebeginTransaction()
method. Note that LoopBack onlysupports database local transactions - only operations against the sametransaction-capable datasource can be grouped into a transaction.
Transaction APIs
The @loopback/repository
package includes TransactionalRepository
interfacebased on EntityCrudRepository
interface. The TransactionalRepository
interface adds a beginTransaction()
API that, for connectors that allow it,will start a new Transaction. The beginTransaction()
function gives access tothe lower-level transaction API, leaving it up to the user to create and managetransaction objects, commit them on success or roll them back at the end of allintended operations. See Handling Transactions belowfor more details.
Handling Transactions
Seethe API reference forfull transaction lower-level API documentation.
Performing operations in a transaction typically involves the following steps:
- Start a new transaction.
- Perform create, read, update, and delete operations in the transaction.
- Commit or rollback the transaction.
Start transaction
Use the beginTransaction()
method to start a new transaction from a repositoryclass using DefaultTransactionalRepository
as a base class.
Here is an example:
import {
Transaction,
DefaultTransactionalRepository,
IsolationLevel,
} from '@loopback/repository';
// assuming there is a Note model extending Entity class, and
// ds datasource which is backed by a transaction enabled
// connector
const repo = new DefaultTransactionalRepository(Note, ds);
// Now we have a transaction (tx)
const tx = await repo.beginTransaction(IsolationLevel.READ_COMMITTED);
You can also extend DefaultTransactionalRepository
for custom classes:
import {inject} from '@loopback/core';
import {
juggler,
Transaction,
DefaultTransactionalRepository,
IsolationLevel,
} from '@loopback/repository';
import {Note, NoteRelations} from '../models';
export class NoteRepository extends DefaultTransactionalRepository<
Note,
typeof Note.prototype.id,
NoteRelations
> {
constructor(@inject('datasources.ds') ds: juggler.DataSource) {
super(Note, ds);
}
}
Isolation levels
When you call beginTransaction()
, you can optionally specify a transactionisolation level. LoopBack transactions support the following isolation levels:
Transaction.READ_UNCOMMITTED
Transaction.READ_COMMITTED
(default)Transaction.REPEATABLE_READ
Transaction.SERIALIZABLE
If you don’t specify an isolation level, the transaction uses READ_COMMITTED .
Important:
Oracle only supports READ_COMMITTED and SERIALIZABLE.
For more information about database-specific isolation levels, see:
- MySQL SET TRANSACTION Syntax
- Oracle Isolation Levels
- PostgreSQL Transaction Isolation
- SQL Server SET TRANSACTION ISOLATION LEVEL
Perform operations in a transaction
To perform create, retrieve, update, and delete operations in the transaction,add the transaction object to the Options
parameter of the standard create()
,update()
,deleteAll()
(and so on) methods.
For example, again assuming a Note
model, repo
transactional repository, andtransaction object tx
created as demonstrated inStart transaction section:
const created = await repo.create({title: 'Groceries'}, {transaction: tx});
const updated = await repo.update(
{title: 'Errands', id: created.id},
{transaction: tx},
);
// commit the transaction to persist the changes
await tx.commit();
Propagating a transaction is explicit by passing the transaction object via theoptions argument for all create, retrieve, update, and delete and relationmethods.
Commit or rollback
Transactions allow you either to commit the transaction and persist the CRUDbehaviour onto the database or rollback the changes. The two methods availableon transaction objects are as follows:
/**
* Commit the transaction
*/
commit(): Promise<void>;
/**
* Rollback the transaction
*/
rollback(): Promise<void>;
Set up timeout
You can specify a timeout (in milliseconds) to begin a transaction. If atransaction is not finished (committed or rolled back) before the timeout, itwill be automatically rolled back upon timeout by default.
For example, again assuming a Note
model and repo
transactional repository,the timeout
can be specified as part of the Options
object passed into thebeginTransaction
method.
const tx: Transaction = await repo.beginTransaction({
isolationLevel: IsolationLevel.READ_COMMITTED,
timeout: 30000, // 30000ms = 30s
});
Avoid long waits or deadlocks
Please be aware that a transaction with certain isolation level will lockdatabase objects. Performing multiple methods within a transactionasynchronously has the great potential to block other transactions (explicit orimplicit). To avoid long waits or even deadlocks, you should:
- Keep the transaction as short-lived as possible
- Don’t serialize execution of methods across multiple transactions