Transactions
Drift has support for transactions and allows multiple statements to run atomically,
so that none of their changes is visible to the main database until the transaction
is finished.
To begin a transaction, call the transaction
method on your database or a DAO.
It takes a function as an argument that will be run transactionally. In the
following example, which deals with deleting a category, we move all todo entries
in that category back to the default category:
Future<void> deleteCategory(Category category) {
return transaction(() async {
// first, move the affected todo entries back to the default category
await (update(todoItems)
..where((row) => row.category.equals(category.id)))
.write(const TodoItemsCompanion(category: Value(null)));
// then, delete the category
await delete(categories).delete(category);
});
}
⚠️ Important things to know about transactions¶
There are a couple of things that should be kept in mind when working with transactions:
- Await all calls: All queries inside the transaction must be
await
-ed. The transaction will complete when the inner method completes. Withoutawait
, some queries might be operating on the transaction after it has been closed! This can cause data loss or runtime crashes. Drift contains some runtime checks against this misuse and will throw an exception when a transaction is used after being closed. A transaction is active during all asynchronous calls made in atransaction
block, so transactions also can't schedule timers or other operations using the database (as those would try to use the transaction after the maintransaction
block has completed). - Different behavior of stream queries: Inside a
transaction
callback, stream queries behave differently. If you're creating streams inside a transaction, check the next section to learn how they behave.
Transactions and query streams¶
Query streams that have been created outside a transaction work nicely together with updates made in a transaction: All changes to tables will only be reported after the transaction completes. Updates inside a transaction don't have an immediate effect on streams, so your data will always be consistent and there aren't any unnecessary updates.
Streams created inside a transaction
block (or in a function that was called inside
a transaction
) block reflect changes made in a transaction immediately.
However, such streams close when the transaction completes.
This behavior is useful if you're collapsing streams inside a transaction, for instance by
calling first
or fold
.
However, we recommend that streams created inside a transaction are not listened to
outside of a transaction. While it's possible, it defeats the isolation principle
of transactions as its state is exposed through the stream.
Nested transactions¶
Starting from drift version 2.0, it is possible to nest transactions on most implementations.
When calling transaction
again inside a transaction
block (directly or indirectly through
method invocations), a nested transaction is created. Nested transactions behave as follows:
- When they start, queries issued in a nested transaction see the state of the database from the outer transaction immediately before the nested transaction was started.
- Writes made by a nested transaction are only visible inside the nested transaction at first. The outer transaction and the top-level database don't see them right away, and their stream queries are not updated.
- When a nested transaction completes successfully, the outer transaction sees the changes made by the nested transaction as an atomic write (stream queries created in the outer transaction are updated once).
- When a nested transaction throws an exception, it is reverted (so in that sense, it behaves just like other transactions). The outer transaction can catch this exception, after it will be in the same state before the nested transaction was started. If it does not catch that exception, it will bubble up and revert that transaction as well.
The following snippet illustrates the behavior of nested transactions:
Future<void> nestedTransactions() async {
await transaction(() async {
await into(categories).insert(CategoriesCompanion.insert(name: 'first'));
// this is a nested transaction:
await transaction(() async {
// At this point, the first category is visible
await into(categories)
.insert(CategoriesCompanion.insert(name: 'second'));
// Here, the second category is only visible inside this nested
// transaction.
});
// At this point, the second category is visible here as well.
try {
await transaction(() async {
// At this point, both categories are visible
await into(categories)
.insert(CategoriesCompanion.insert(name: 'third'));
// The third category is only visible here.
throw Exception('Abort in the second nested transaction');
});
} on Exception {
// We're catching the exception so that this transaction isn't reverted
// as well.
}
// At this point, the third category is NOT visible, but the other two
// are. The transaction is in the same state as before the second nested
// `transaction()` call.
});
// After the transaction, two categories are visible.
}
Supported implementations¶
Nested transactions require support by the database implementation you're using with drift. All popular implementations support this feature, including:
- A
NativeDatabase
frompackage:drift/native.dart
- A
WasmDatabase
frompackage:drift/wasm.dart
- The sql.js-based
WebDatabase
frompackage:drift/web.dart
- A
SqfliteDatabase
frompackage:drift_sqflite
.
Further, nested transactions are supported through remote database connections (e.g. isolates or web workers) if the server uses a database implementation that supports them.