Skip to content

事务(Transactions)

摘要

原子化地运行多个语句。


Drift 支持事务,并允许多个语句原子化地运行,这样在事务完成之前,它们的所有更改对主数据库都是不可见的。要开始一个事务,请在你的数据库或 DAO 上调用 transaction 方法。它接受一个将以事务方式运行的函数作为参数。在下面的示例中,我们处理删除一个类别,我们将该类别中的所有待办事项移回默认类别:

dart
Future<void> deleteCategory(Category category) {
  return transaction(() async {
    // 首先,将受影响的待办事项移回默认类别
    await (update(todoItems)
          ..where((row) => row.category.equals(category.id)))
        .write(const TodoItemsCompanion(category: Value(null)));

    // 然后,删除该类别
    await delete(categories).delete(category);
  });
}

⚠️ 关于事务的重要事项

在使用事务时,有几件事需要牢记:

  1. 等待所有调用:事务内的所有查询都必须被 await。当内部方法完成时,事务将完成。如果没有 await,一些查询可能会在事务关闭后仍在事务上操作!这可能导致数据丢失或运行时崩溃。Drift 包含一些针对此滥用的运行时检查,并在事务关闭后使用时会抛出异常。在 transaction 块中进行的所有异步调用期间,事务都是活动的,因此事务也不能安排使用数据库的计时器或其他操作(因为那些操作会尝试在主 transaction 块完成后使用事务)。
  2. 流查询的不同行为:在 transaction 回调内部,流查询的行为有所不同。如果你在事务内部创建流,请查看下一节以了解它们的行为方式。

事务和查询流

在事务外部创建的查询流与在事务中进行的更新很好地协同工作:对表的所有更改只会在事务完成后报告。事务内的更新对流没有立即的影响,因此你的数据将始终保持一致,并且没有不必要的更新。

transaction 块内部(或在 transaction 内部调用的函数中)创建的流会立即反映在事务中所做的更改。但是,此类流在事务完成时会关闭。

如果你在事务内部折叠流(例如,通过调用 firstfold),此行为很有用。但是,我们建议不要在事务外部侦听在事务内部创建的流。虽然这是可能的,但它违背了事务的隔离原则,因为它的状态通过流被暴露了。

嵌套事务

从 drift 2.0 版开始,可以在大多数实现上嵌套事务。当在 transaction 块内再次调用 transaction 时(直接或通过方法调用间接调用),会创建一个_嵌套事务_。嵌套事务的行为如下:

  • 当它们开始时,在嵌套事务中发出的查询会看到在嵌套事务开始之前外部事务的数据库状态。
  • 嵌套事务进行的写入最初只在嵌套事务内部可见。外部事务和顶层数据库不会立即看到它们,并且它们的流查询也不会更新。
  • 当嵌套事务成功完成时,外部事务会将嵌套事务进行的更改视为原子写入(在外部事务中创建的流查询会更新一次)。
  • 当嵌套事务抛出异常时,它会被回滚(因此从这个意义上说,它的行为就像其他事务一样)。外部事务可以捕获此异常,之后它将处于嵌套事务开始之前的相同状态。如果它不捕获该异常,它将冒泡并回滚该事务。

以下代码段说明了嵌套事务的行为:

dart
Future<void> nestedTransactions() async {
  await transaction(() async {
    await into(categories).insert(CategoriesCompanion.insert(name: 'first'));

    // 这是一个嵌套事务:
    await transaction(() async {
      // 此时,第一个类别是可见的
      await into(
        categories,
      ).insert(CategoriesCompanion.insert(name: 'second'));
      // 在这里,第二个类别只在此嵌套事务内部可见。
    });

    // 此时,第二个类别在这里也可见。

    try {
      await transaction(() async {
        // 此时,两个类别都可见
        await into(
          categories,
        ).insert(CategoriesCompanion.insert(name: 'third'));
        // 第三个类别只在这里可见。
        throw Exception('在第二个嵌套事务中中止');
      });
    } on Exception {
      // 我们正在捕获异常,以便此事务也不会被回滚。
    }

    // 此时,第三个类别是不可见的,但其他两个是可见的。事务处于与第二个嵌套
    // `transaction()` 调用之前的状态相同。
  });

  // 事务结束后,两个类别是可见的。
}

支持的实现

嵌套事务需要你与 drift 一起使用的数据库实现的支持。所有流行的实现都支持此功能,包括:

  • 来自 package:drift/native.dartNativeDatabase
  • 来自 package:drift/wasm.dartWasmDatabase
  • 来自 package:drift/web.dart 的基于 sql.js 的 WebDatabase
  • 来自 package:drift_sqfliteSqfliteDatabase

此外,如果服务器使用支持它们的数据库实现,则通过远程数据库连接(例如 isolate 或 web worker)也支持嵌套事务。