transaction<T> method

Future<T> transaction<T>(
  1. Future<T> action(), {
  2. bool requireNew = false,
})

Executes action in a transaction, which means that all its queries and updates will be called atomically.

Returns the value of action. When action throws an exception, the transaction will be reset and no changes will be applied to the databases. The exception will be rethrown by transaction.

The behavior of stream queries in transactions depends on where the stream was created:

  • streams created outside of a transaction block: The stream will update with the tables modified in the transaction after it completes successfully. If the transaction fails, the stream will not update.
  • streams created inside a transaction block: The stream will update for each write in the transaction. When the transaction completes, successful or not, streams created in it will close. Writes happening outside of this transaction will not affect the stream.

Starting from drift version 2.0, nested transactions are supported on most database implementations (including NativeDatabase, WebDatabase, WasmDatabase, SqfliteQueryExecutor, databases relayed through isolates or web workers). When calling transaction inside a transaction block on supported database implementations, a new transaction will be started. For backwards-compatibility, the current transaction will be re-used if a nested transaction is started with a database implementation not supporting nested transactions. The requireNew parameter can be set to instead turn this case into a runtime error.

Nested transactions are conceptionally similar to regular, top-level transactions in the sense that their writes are not seen by users outside of the transaction until it is commited. However, their behavior around completions is different:

  • When a nested transaction completes, nothing is being persisted right away. The parent transaction can now see changes from the child transaction and continues to run. When the outermost transaction completes, its changes (including changes from child transactions) are written to the database.
  • When a nested transaction is aborted (which happens due to exceptions), only changes in that inner transaction are reverted. The outer transaction can continue to run if it catched the exception thrown by the inner transaction when it aborted.

See also:

Implementation

Future<T> transaction<T>(Future<T> Function() action,
    {bool requireNew = false}) async {
  final resolved = resolvedEngine;

  // Are we about to start a nested transaction?
  if (resolved is Transaction) {
    final executor = resolved.executor as TransactionExecutor;
    if (!executor.supportsNestedTransactions) {
      if (requireNew) {
        throw UnsupportedError('The current database implementation does '
            'not support nested transactions.');
      } else {
        // Just run the block in the current transaction zone.
        return action();
      }
    }
  }

  return await resolved.doWhenOpened((executor) {
    final transactionExecutor = executor.beginTransaction();
    final transaction = Transaction(this, transactionExecutor);

    return _runConnectionZoned(transaction, () async {
      var success = false;
      try {
        await transactionExecutor.ensureOpen(attachedDatabase);

        final result = await action();
        success = true;
        return result;
      } catch (e, s) {
        await transactionExecutor.rollbackAfterException(e, s);

        // pass the exception on to the one who called transaction()
        rethrow;
      } finally {
        if (success) {
          try {
            await transaction.complete();
          } catch (e, s) {
            // Couldn't commit -> roll back then.
            await transactionExecutor.rollbackAfterException(e, s);
            rethrow;
          }
        }
        await transaction.disposeChildStreams();
      }
    });
  });
}