diff --git a/docs/lib/snippets/transactions.dart b/docs/lib/snippets/transactions.dart new file mode 100644 index 00000000..31da3f57 --- /dev/null +++ b/docs/lib/snippets/transactions.dart @@ -0,0 +1,56 @@ +import 'package:drift/drift.dart'; +import 'tables/filename.dart'; + +extension Snippets on MyDatabase { + // #docregion deleteCategory + Future 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 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 +} diff --git a/docs/pages/docs/transactions.md b/docs/pages/docs/transactions.md index 94789759..35086da8 100644 --- a/docs/pages/docs/transactions.md +++ b/docs/pages/docs/transactions.md @@ -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. diff --git a/drift/lib/src/runtime/api/connection_user.dart b/drift/lib/src/runtime/api/connection_user.dart index c7b24ca1..0acc5ff1 100644 --- a/drift/lib/src/runtime/api/connection_user.dart +++ b/drift/lib/src/runtime/api/connection_user.dart @@ -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