トランザクション
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);
});
}
トランザクションを扱う際に気をつけるべきことがいくつかあります:
-
すべ ての呼び出しを待つ: トランザクション内のすべてのクエリは
await
されなければなりません。 トランザクションは、内部のメソッドが完了したときに完了します。await
を使用しないと、トランザクションが終了した後でもクエリが実行される可能性があります! これはデータ損失やランタイムクラッシュの原因となります。Driftにはこのような誤用に対する実行時チェック機能があり、トランザクションがクローズされた後に使用されると例外がスローされます。 -
Streamクエリの動作の違い:
transaction
コールバック内では、Streamクエリの動作が異なります。 トランザクションの内部でストリームを作成する場合は、次のセクションでその動作を確認してください。
トランザクションとStreamクエリ
トランザクション外で作成されたStreamクエリは、トランザクション内で行われた更新とうまく連動します。テーブルへのすべての変更は、トランザクションが完了した後にのみ報告されます。 トランザクション内の更新はStreamに即座に影響しないため、データは常に一貫性が保たれ、不要な更新は発生しません。
transaction
ブロック内(またはtransaction
内で呼び出された関数内)で作成されたStreamは、トランザクション内で行われた変更を即座にブロックに反映します。
しかし、このよ うなStreamはトランザクションが完了すると閉じられます。
この動作は、たとえばfirst
やfold
を呼び出すなどして、トランザクション内で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.dart
のsql.js
ベースのWebDatabase
package:drift_sqflite.dart
のSqfliteDatabase
さらに、サーバーがネストされたトランザクションをサポートするデータベース実装を使用している場合、ネストされたトランザクションはリモートデータベース接続(isolateやweb worker)を通じてサポートされます。