Transactions#

Introduction#

DBLayer supports manual and closure transactions, including nested transaction levels where savepoints are available for the active driver.

Closure Transaction#

DB::transaction(function (): void {
    DB::table('accounts')->where('id', '=', 1)->update(['balance' => 900]);
    DB::table('accounts')->where('id', '=', 2)->update(['balance' => 1100]);
});

If an exception is thrown inside the callback, DBLayer rolls back and rethrows.

Manual Transaction#

DB::beginTransaction();
try {
    DB::table('orders')->insert(['user_id' => 1, 'total' => 100]);
    DB::commit();
} catch (Throwable $e) {
    DB::rollBack();
    throw $e;
}

Retry Attempts#

Use retry attempts for transient failures:

DB::transaction(function (): void {
    // critical write path
}, attempts: 3);

When retries are enabled, write logic must be idempotent or safely repeatable. Prefer transaction-level retry for deadlocks/serialization failures over retrying standalone non-idempotent statements.

Read-Only Transactions#

DB::readOnlyTransaction(function ($connection): int {
    return (int) $connection->scalar('select count(*) from reports');
});

Driver behavior:

  • PostgreSQL: best-effort SET TRANSACTION READ ONLY.

  • MySQL/MariaDB: best-effort SET TRANSACTION READ ONLY.

  • SQLite: safe no-op (no transaction-scoped read-only toggle).

Execution Budgets#

Combine transaction logic with query-level timeout/deadline wrappers:

DB::withQueryTimeout(500, function (): void {
    DB::transaction(function (): void {
        DB::select('select 1');
    });
});

Note

Timeouts and deadlines are query-execution controls, not transaction-level lock-time guarantees. Database engine behavior still applies.

Introspection#

  • DB::transactionLevel()

  • DB::transactionStats()