mirror of https://github.com/AMT-Cheif/drift.git
Document nested transactions
This commit is contained in:
parent
4959ec6235
commit
c74f5d4cb4
|
@ -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
|
||||||
|
}
|
|
@ -6,9 +6,11 @@ data:
|
||||||
|
|
||||||
template: layouts/docs/single
|
template: layouts/docs/single
|
||||||
aliases:
|
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,
|
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
|
so that none of their changes is visible to the main database until the transaction
|
||||||
is finished.
|
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
|
following example, which deals with deleting a category, we move all todo entries
|
||||||
in that category back to the default category:
|
in that category back to the default category:
|
||||||
|
|
||||||
```dart
|
{% include "blocks/snippet" snippets = snippets name = "deleteCategory" %}
|
||||||
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)],
|
|
||||||
);
|
|
||||||
|
|
||||||
// then, delete the category
|
## ⚠️ Important things to know about transactions {#-gotchas}
|
||||||
await delete(categories).delete(category);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚠️ Gotchas
|
|
||||||
There are a couple of things that should be kept in mind when working with transactions:
|
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
|
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
|
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.
|
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
|
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
|
differently. If you're creating streams inside a transaction, check the next section to learn how
|
||||||
they behave.
|
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
|
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, 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
|
Streams created _inside_ a `transaction` block (or in a function that was called inside
|
||||||
a different story. Notably, they
|
a `transaction`) block reflect changes made in a transaction immediately.
|
||||||
|
However, such streams close when the transaction completes.
|
||||||
- reflect on changes made in the transaction immediately
|
|
||||||
- complete when the transaction completes
|
|
||||||
|
|
||||||
This behavior is useful if you're collapsing streams inside a transaction, for instance by
|
This behavior is useful if you're collapsing streams inside a transaction, for instance by
|
||||||
calling `first` or `fold`.
|
calling `first` or `fold`.
|
||||||
However, we recommend that streams created _inside_ a transaction are not listened to
|
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
|
_outside_ of a transaction. While it's possible, it defeats the isolation principle
|
||||||
of transactions as its state is exposed through the stream.
|
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.
|
||||||
|
|
|
@ -428,9 +428,11 @@ abstract class DatabaseConnectionUser {
|
||||||
/// outside of this transaction will not affect the stream.
|
/// outside of this transaction will not affect the stream.
|
||||||
///
|
///
|
||||||
/// Starting from drift version 2.0, nested transactions are supported on most
|
/// Starting from drift version 2.0, nested transactions are supported on most
|
||||||
/// database implementations (including `NativeDatabase`, TODO list). When
|
/// database implementations (including `NativeDatabase`, `WebDatabase`,
|
||||||
/// calling [transaction] inside a [transaction] block on supported database
|
/// `WasmDatabase`, `SqfliteQueryExecutor`, databases relayed through
|
||||||
/// implementations, a new transaction will be started.
|
/// 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
|
/// For backwards-compatibility, the current transaction will be re-used if
|
||||||
/// a nested transaction is started with a database implementation not
|
/// a nested transaction is started with a database implementation not
|
||||||
/// supporting nested transactions. The [requireNew] parameter can be set to
|
/// supporting nested transactions. The [requireNew] parameter can be set to
|
||||||
|
|
Loading…
Reference in New Issue