Skip to main content

Transactions

Hysteria ORM provides robust transaction support for SQL databases, allowing you to group multiple operations into a single atomic unit. Transactions ensure data consistency and integrity, supporting features like rollback, isolation levels, nested, concurrent, and global transactions.

Basic Usage

Use SqlDataSource.useTransaction to run operations within a transaction. If an error is thrown, the transaction is rolled back automatically.

await SqlDataSource.useTransaction(async (trx) => {
await User.insert({ ...UserFactory.getCommonUserData() }, { trx });
});

If an error occurs, changes are not committed:

await SqlDataSource.useTransaction(async (trx) => {
await User.insert({ ...UserFactory.getCommonUserData() }, { trx });
throw new Error("Test error"); // Transaction is rolled back automatically
});

Custom Isolation Level

You can specify a transaction isolation level:

await SqlDataSource.useTransaction(
async (trx) => {
await User.insert({ ...UserFactory.getCommonUserData() }, { trx });
},
{ isolationLevel: "SERIALIZABLE" },
);

Manual Transaction Control

Start, commit, and rollback transactions manually:

const trx = await SqlDataSource.startTransaction();
await User.insert({ ...UserFactory.getCommonUserData() }, { trx });
await trx.commit();

Rollback on error:

const trx = await SqlDataSource.startTransaction();
try {
await User.insert({ ...UserFactory.getCommonUserData() }, { trx });
throw new Error("fail");
await trx.commit();
} catch {
await trx.rollback();
}

Nested Transactions

Some databases (e.g., PostgreSQL, MySQL) support nested transactions. SQLite does not.

const outerTrx = await SqlDataSource.startTransaction();
await User.insert({ ...UserFactory.getCommonUserData() }, { trx: outerTrx });

const innerTrx = await SqlDataSource.startTransaction();
await User.insert({ ...UserFactory.getCommonUserData() }, { trx: innerTrx });
await innerTrx.rollback(); // Only inner changes are rolled back
await outerTrx.commit(); // Outer changes are committed

Concurrent Transactions

You can run multiple transactions in parallel (except in SQLite):

const trx1 = await SqlDataSource.startTransaction();
const trx2 = await SqlDataSource.startTransaction();
await User.insert({ ...UserFactory.getCommonUserData() }, { trx: trx1 });
await User.insert({ ...UserFactory.getCommonUserData() }, { trx: trx2 });
await trx1.commit();
await trx2.commit();

Global Transactions

For integration tests or batch operations, you can use global transactions:

await SqlDataSource.startGlobalTransaction();
// Automatically uses the transaction from the SqlDataSource global transaction
await User.insert({ ...UserFactory.getCommonUserData() });
await SqlDataSource.commitGlobalTransaction();

Rollback global transaction:

await SqlDataSource.startGlobalTransaction();
await User.insert({ ...UserFactory.getCommonUserData() });
await SqlDataSource.rollbackGlobalTransaction();

Error Handling and Transaction State

You can enforce error throwing if a transaction is inactive:

const trx = await SqlDataSource.startTransaction();
await trx.rollback();
await trx.rollback({ throwErrorOnInactiveTransaction: true }); // Throws HysteriaError

Or suppress errors:

const trx = await SqlDataSource.startTransaction();
await trx.rollback();
await trx.rollback({ throwErrorOnInactiveTransaction: false }); // No error

Notes

  • SQLite does not support nested or concurrent transactions.
  • Always pass the trx object to model methods to ensure operations are part of the transaction.
  • Use isolation levels for advanced consistency requirements.

See also: