Document nested transactions

This commit is contained in:
Simon Binder 2022-06-25 15:51:28 +02:00
parent 4959ec6235
commit c74f5d4cb4
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
3 changed files with 108 additions and 25 deletions

View File

@ -0,0 +1,56 @@
import 'package:drift/drift.dart';
import 'tables/filename.dart';
extension Snippets on MyDatabase {
// #docregion deleteCategory
Future<void> deleteCategory(Category category) {
return transaction(() async {
// first, move the affected todo entries back to the default category
await (update(todos)..where((row) => row.category.equals(category.id)))
.write(const TodosCompanion(category: Value(null)));
// then, delete the category
await delete(categories).delete(category);
});
}
// #enddocregion deleteCategory
// #docregion nested
Future<void> nestedTransactions() async {
await transaction(() async {
await into(categories)
.insert(CategoriesCompanion.insert(description: 'first'));
// this is a nested transaction:
await transaction(() async {
// At this point, the first category is visible
await into(categories)
.insert(CategoriesCompanion.insert(description: '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(description: '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.
}
// #enddocregion nested
}

View File

@ -6,9 +6,11 @@ data:
template: layouts/docs/single
aliases:
- /transactions/
- /transactions/
---
{% assign snippets = "package:moor_documentation/snippets/transactions.dart.excerpt.json" | readString | json_decode %}
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.
@ -17,28 +19,16 @@ 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:
```dart
Future deleteCategory(Category category) {
return transaction(() async {
// first, move the affected todo entries back to the default category
await customUpdate(
'UPDATE todos SET category = NULL WHERE category = ?',
updates: {todos},
variables: [Variable.withInt(category.id)],
);
{% include "blocks/snippet" snippets = snippets name = "deleteCategory" %}
// then, delete the category
await delete(categories).delete(category);
});
}
```
## ⚠️ Gotchas
## ⚠️ Important things to know about transactions {#-gotchas}
There are a couple of things that should be kept in mind when working with transactions:
1. __Await all calls__: All queries inside the transaction must be `await`-ed. The transaction
will complete when the inner method completes. Without `await`, 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.
2. __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.
@ -49,14 +39,49 @@ updates made in a transaction: All changes to tables will only be reported after
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.
With streams created _inside_ a `transaction` block (or a nested call in there), it's
a different story. Notably, they
- reflect on changes made in the transaction immediately
- complete when the transaction completes
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:
{% include "blocks/snippet" snippets = snippets name = "nested" %}
### Supported implementations
Nested transactions require support by the database implementation you're using with drift.
All popular implementations support this feature, including:
- A `NativeDatabase` from `package:drift/native.dart`
- A `WasmDatabase` from `package:drift/wasm.dart`
- The sql.js-based `WebDatabase` from `package:drift/web.dart`
- A `SqfliteDatabase` from `package: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.

View File

@ -428,9 +428,11 @@ abstract class DatabaseConnectionUser {
/// 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`, TODO list). When
/// calling [transaction] inside a [transaction] block on supported database
/// implementations, a new transaction will be started.
/// 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