Skip to main content

トランザクション

Driftはトランザクションをサポートしており、複数のステートメントをアトミックに実行することができます。 トランザクションを開始するには、データベースまたはDAOのtransactionメソッドを呼び出します。 引数として、トランザクションを実行する関数を取ります。次の例では、カテゴリーを削除し、そのカテゴリーのすべてのTodoエントリーをデフォルトのカテゴリーに戻します。

Future<void> deleteCategory(Category category) {
return transaction(() async {
// まず、影響を受けるTodoエントリーをデフォルトのカテゴリーに戻します
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にはこのような誤用に対する実行時チェック機能があり、トランザクションがクローズされた後に使用されると例外がスローされます。

  2. Streamクエリの動作の違い: transactionコールバック内では、Streamクエリの動作が異なります。 トランザクションの内部でストリームを作成する場合は、次のセクションでその動作を確認してください。

トランザクションとStreamクエリ

トランザクション外で作成されたStreamクエリは、トランザクション内で行われた更新とうまく連動します。テーブルへのすべての変更は、トランザクションが完了した後にのみ報告されます。 トランザクション内の更新はStreamに即座に影響しないため、データは常に一貫性が保たれ、不要な更新は発生しません。

transactionブロック内(またはtransaction内で呼び出された関数内)で作成されたStreamは、トランザクション内で行われた変更を即座にブロックに反映します。 しかし、このようなStreamはトランザクションが完了すると閉じられます。

この動作は、たとえばfirstfoldを呼び出すなどして、トランザクション内でStreamを折りたたむ場合に便利です。 しかし、トランザクション内部で作成されたStreamをトランザクション外部で開かないことをおすすめします。 可能ではありますが、Streamを通して状態が公開されるため、トランザクションの分離原則を破ってしまいます。

ネストされたトランザクション

Driftバージョン2.0から、ほとんどの実装でトランザクションのネストが可能になりました。 transactionブロックの内部でtransactionを再度呼び出すと(直接またはメソッド呼び出しを通じて間接的に)、ネストされたトランザクションが作成されます。 ネストされたトランザクションは以下のように動作します:

  • ネストされたトランザクションで発行されたクエリは、ネストされたトランザクションが開始される直前の外部トランザクションのデータベースの状態を見る
  • ネストされたトランザクションで行われた書き込みは、最初はネストされたトランザクションの内部でしか見ることができません。 外部のトランザクションとトップレベルデータベースはそれらをすぐに見ることはできず、それらのStreamクエリは更新されません。
  • ネストされたトランザクションが正常に完了すると、外部のトランザクションは、ネストされたトランザクションによって行われた変更をアトミック書き込みとして認識します(外部のトランザクションで作成されたStreamクエリは1回更新されます)
  • ネストされたトランザクションが例外を投げると、それは元に戻されます(その意味では、他のトランザクションと同じように振る舞います)。 外部のトランザクションは、ネストされたトランザクションが開始される前と同じ状態になった後、この例外をキャッチすることができます。 もしその例外をキャッチしなければ、そのトランザクションもバブルアップして戻されます。

次のスニペットは、ネストされたトランザクションの動作を示しています:

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'));
// ネストされたトランザクション内だけ、2つ目のカテゴリが見える
});

// この時点で、2つ目のカテゴリが見えるようになる

try {
await transaction(() async {
// この時点で, 両方のカテゴリが滅入る
await into(categories)
.insert(CategoriesCompanion.insert(name: 'third'));
// ここでのみ3番目のカテゴリが見える(トランザクション内だけ)
throw Exception('Abort in the second nested transaction');
});
} on Exception {
// このトランザクションが元に戻らないように、例外をキャッチしています
}

// この時点では、3番目のカテゴリは見えず、1,2番目のカテゴリは見える
// エラーが起きた場合、トランザクションの操作は元に戻る
});

// トランザクション終了時、2つのカテゴリが見えている
}

サポートされる実装

ネストされたトランザクションは、Drift で使用するデータベース実装がサポートしている必要があります。一般的な実装はすべてこの機能をサポートしています:

  • package:drift/native.dartに含まれるNativeDatabase
  • package:drift/wasm.dartに収録されているWasmDatabase
  • package:drift/web.dartsql.jsベースのWebDatabase
  • package:drift_sqflite.dartSqfliteDatabase

さらに、サーバーがネストされたトランザクションをサポートするデータベース実装を使用している場合、ネストされたトランザクションはリモートデータベース接続(isolateやweb worker)を通じてサポートされます。