iT邦幫忙

2025 iThome 鐵人賽

DAY 19
0
Mobile Development

《30 天 Flutter:跨平台 AI 行程規劃 App》系列 第 19

Day 19 - Drift 實戰:Transaction 與 Batch 的效能與安全實踐

  • 分享至 

  • xImage
  •  

前一天,我處理了行程的 CRUD 功能,這一切看起來都很順利。但當我動手實作 Day 13 的行程的拖曳排序Day 15 AI 行程生成批次存入 時,才發現背後的資料庫操作,遠比我想像中複雜得多。

為了讓使用者能順暢地拖曳並重新排序行程,我必須確保資料庫的更新不僅要正確,還要有效率。一開始,我遇到了資料不同步的麻煩,這讓我體會到 Transaction 的重要性。當我解決了這個問題後,又發現效能不佳,這才讓我找到了 Batch 這個解決方案。

在今天的文章中,我將分享這段解決問題的心得。從為什麼會需要 Transaction 到如何用 Batch 提升效能,我會一步步帶大家走過我發現和解決這些問題的過程。


資料一致性的守護者:Transaction

Day 13 的行程的拖曳排序 為例,需要一次性更新多筆資料,例如重新排序某一天的活動。若僅部分更新成功,資料就可能出現不一致(例如:行程順序錯亂或時間對不上)。

為避免這種狀況,我們需要把所有更新視為一個整體的原子性操作。這正是 Drift 的 transaction 所提供的功能:

  • 原子性:交易區塊內的所有操作要嘛全部成功,要嘛全部失敗。
  • 回滾機制:只要有任何一筆更新失敗,交易會自動回滾,資料庫會恢復到「交易開始前」的狀態(不會影響交易之外已提交的資料)。

以下是一個簡化範例,展示如何用 transaction 確保更新過程的可靠性:

// activities_dao.dart

Future<void> reorderDayActivities(
  DateTime dayStartTime,
  List<Activity> newDayActivities,
) async {
  // 將所有更新操作包裝在同一個交易中
  await transaction(() async {
    for (final activity in newDayActivities) {
      await (update(activitiesTable)
        ..where((tbl) => tbl.id.equals(activity.id)))
        .write(ActivitiesTableCompanion(
          sortOrder: Value(activity.sortOrder),
          startTime: Value(activity.startTime),
        ));
    }
  });
}

透過 transaction,即使在更新過程中發生錯誤,整個資料仍能保持一致,讓使用者不會遇到「更新到一半就壞掉」的情況。


提升效率的關鍵:Batch

前面我們透過 transaction 確保了資料一致性,但在效率上仍有改進空間。原因在於:在迴圈中,每一次 await ... .write(...) 都會觸發一次 SQLite 呼叫。假設 newDayActivities 有 100 筆資料,就會發出 100 次更新請求,造成大量的資料庫 I/O 與 SQL parser 開銷,效率不佳。

這時候就可以使用 batch。它能將多筆更新指令打包成一次請求,讓 SQLite 在同一個交易裡一次性處理所有更新,大幅減少呼叫次數並提升效能。

以下是優化後的範例:

Future<void> reorderDayActivities({
  required DateTime dayStartTime,
  required List<Activity> newDayActivities,
}) async {
  // 使用 batch,一次性提交所有更新
  await batch((batch) {
    for (final activity in newDayActivities) {
      batch.update(
        activitiesTable,
        ActivitiesTableCompanion(
          sortOrder: Value(activity.sortOrder),
          startTime: Value(activity.startTime),
        ),
        where: (tbl) => tbl.id.equals(activity.id),
      );
    }
  });
}

透過 batch,不僅能保持資料的一致性,還能顯著降低資料庫呼叫的次數,達到 「可靠又高效」 的效果。


Transaction + Batch:打造可靠又高效的複合操作

batch 本身雖然具備原子性,但當你需要處理多個不同類型、且需要相互關聯的資料庫操作時,就必須將 batch 包在一個更外層的 transaction 裡。

Day 15 AI 行程生成 的實作中,我們就是使用了這個模式。我們會一口氣產生一個完整的行程,並同時使用 transactionbatch 功能將行程、活動及子活動一次性存入資料庫。

這樣做有兩個主要優點:

  • 資料完整性:這確保了整個行程、所有活動與子活動都能同時成功存入。如果其中任何一個步驟失敗,整個操作都會被回滾,避免資料庫中出現不完整的資料。
  • 效能提升:透過 batch,我們可以將多個「新增」指令打包成一個單一的資料庫請求,大幅減少資料庫的連線和通訊時間,讓儲存速度更快。

以下是一個簡化範例,展示如何將多個操作包裝在一個交易中,並在內部使用批次處理:

/// 一次性儲存整個行程,包含所有活動及子活動
Future<void> saveTripWithActivities(Trip trip) async {
  await transaction(() async {
    // 步驟 1: 儲存 Trip 本身,並取得其 ID
    final tripId = await into(tripsTable).insert(...);

    // 步驟 2: 儲存每個活動及其子活動
    for (final activity in trip.activities) {
      // 先儲存活動,並取得其 ID
      final activityId = await into(activitiesTable).insert(...);

      // 接著批次儲存該活動的所有子活動
      if (activity.childActivities.isNotEmpty) {
        await batch((batch) {
          for (final childActivity in activity.childActivities) {
            batch.insert(...);
          }
        });
      }
    }
  });
}

透過 transaction + batch 的組合,我們不僅能確保資料的完整性,還能顯著提升複雜寫入操作的效能,達到 「可靠又高效」 的目標。


今日成果

單日行程拖曳 跨日行程拖曳 AI 行程生成(有加速)

上一篇
Day 18 - Drift 實戰:從 UI 介面到 DB Migration 的跌跌撞撞
系列文
《30 天 Flutter:跨平台 AI 行程規劃 App》19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言