前一天,我處理了行程的 CRUD 功能,這一切看起來都很順利。但當我動手實作 Day 13 的行程的拖曳排序 及 Day 15 AI 行程生成批次存入 時,才發現背後的資料庫操作,遠比我想像中複雜得多。
為了讓使用者能順暢地拖曳並重新排序行程,我必須確保資料庫的更新不僅要正確,還要有效率。一開始,我遇到了資料不同步的麻煩,這讓我體會到 Transaction 的重要性。當我解決了這個問題後,又發現效能不佳,這才讓我找到了 Batch 這個解決方案。
在今天的文章中,我將分享這段解決問題的心得。從為什麼會需要 Transaction
到如何用 Batch
提升效能,我會一步步帶大家走過我發現和解決這些問題的過程。
以 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
,即使在更新過程中發生錯誤,整個資料仍能保持一致,讓使用者不會遇到「更新到一半就壞掉」的情況。
前面我們透過 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
,不僅能保持資料的一致性,還能顯著降低資料庫呼叫的次數,達到 「可靠又高效」 的效果。
batch
本身雖然具備原子性,但當你需要處理多個不同類型、且需要相互關聯的資料庫操作時,就必須將 batch
包在一個更外層的 transaction
裡。
在 Day 15 AI 行程生成 的實作中,我們就是使用了這個模式。我們會一口氣產生一個完整的行程,並同時使用 transaction
與 batch
功能將行程、活動及子活動一次性存入資料庫。
這樣做有兩個主要優點:
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 行程生成(有加速) |
---|---|---|
![]() |
![]() |
![]() |