Skip to content

transactions

Alexey Borzov edited this page Jul 18, 2020 · 3 revisions

Transactions

Connection class has usual methods for controlling transactions and savepoints: beginTransaction(), commit(), rollback(), inTransaction(), createSavepoint(), releaseSavepoint(), rollbackToSavepoint(). It also provides a convenience method for running a callable atomically, reducing the need for boilerplate code that deals with opening and closing transactions:

$connection->atomic(function () {
    // do some stuff
});

is roughly equivalent to

$connection->beginTransaction();
try {
    // do some stuff
    $connection->commit();
} catch (\Throwable $e) {
    $connection->rollback();
    throw $e;
}

atomic() calls can be nested, the inner call may create a savepoint (this behaviour is controlled by a second argument to atomic()) and thus be rolled back without affecting the whole transaction:

// note that connection object will be passed as an argument to callback
$connection->atomic(function (Connection $connection) {
    storeSomeRecords();
    try {
        // We know that the function may fail due to some unique constraint violation
        // and are perfectly fine with that, so request a savepoint for inner atomic block 
        $connection->atomic(function () {
            populateSomeDictionaries();
        }, true);
    } catch (ConstraintViolationException $e) {
        // even if the inner atomic() failed the outer atomic may proceed
    }
    storeSomethingElse();
});

Note: The example above shows the correct way to catch errors with atomic, that is around atomic() call. As atomic() looks at exceptions to know whether callback succeeded or failed, catching and handling exceptions around individual queries will break that logic. If necessary, add another atomic() call for these queries.

Internally atomic() does the following

  • opens a transaction in the outermost atomic() call;
  • creates a savepoint when entering an inner atomic() call;
  • performs a callback;
  • releases or rolls back to the savepoint when exiting an inner call;
  • commits or rolls back the transaction when exiting the outermost call.

If savepoint wasn't created for an inner call, atomic() will perform the rollback when exiting the first parent call with a savepoint if there is one, and the outermost call otherwise.

Note: if transaction was already open before an outermost atomic() call made with $savepoint = false, it will not be committed or rolled back on exit, you'll have to do it explicitly. If an error happens, atomic() will, however, mark the transaction "for rollback only".

Performing actions after transaction

Sometimes you need to perform an action related to the current database transaction, but only if the transaction successfully commits, e.g. send an email notification, or invalidate a cache. You may also need to do some cleanup after a rollback.

Connection has methods for registering callbacks that will run after commit and rollback: onCommit() and onRollback(). You can only use these methods inside atomic(), outside you'll get BadMethodCallException.

$connection->atomic(function (Connection $connection) {
    $connection->onCommit(function () {
        sendAnEmail();
        resetACache();
    });
    $connection->onRollback(function () {
        resetSomeModelProperties();
        clearSomeFiles();
    });
});

Savepoints created by nested atomic() calls are handled correctly. If inner atomic() call fails, and the transaction is rolled back to savepoint, then onCommit() callbacks registered within that call and nested atomic() calls will not run after transaction commit. Their onRollback() callbacks will run instead.

Callbacks are executed outside the transaction after a commit or rollback. This means that an error in onCommit() callback will not cause a rollback.

Note: While Connection takes reasonable precautions to run onRollback() callbacks in case of implicit rollback (lost connection to DB while in transaction, script exit() while in transaction), it is possible that the script terminates in such a way that callbacks will not run.