今天我持續透過 Gemini 職涯導師模式,學習資料模型與 CRUD 操作。將統整我今日的學習內容,並簡要分享 Gemini 職涯導師在引導學習全新套件時的實際效果。
透過 DAO 進行封裝
程式碼:
// trips_dao.dart
Future<int> addTrip(String title) {
return into(tripsTable).insert(TripsTableCompanion.insert(name: title));
}
// trip_provider.dart
await db.tripsDao.addTrip(title);
DAO(Data Access Object)的寫法看似多包了一層,但它帶來了幾個顯著的好處:
撈取行程列表(不含活動)
// trips_dao.dart
@DriftAccessor(tables: [TripsTable])
class TripsDao extends DatabaseAccessor<AppDatabase> with _$TripsDaoMixin {
TripsDao(super.attachedDatabase);
Future<List<Trip>> getAllTrips() {
return select(tripsTable).get().then((rows) {
return rows
.map(
(entry) => Trip(
id: entry.id,
title: entry.name,
activities: [], // 活動會由 ActivitiesDao 負責載入
),
)
.toList();
});
}
}
// trip_provider.dart
final List<Trip> trips = await db.tripsDao.getAllTrips();
為何要在 DAO 中執行資料轉換?
雖然我們學會了基本的資料查詢,但在處理複雜的關聯資料時,原先的寫法會引發一個嚴重的效能問題——N+1 查詢問題。
這就像是你去超市購物,但每拿一樣東西就跑去結一次帳,重複多次。原先的寫法在撈取行程及其活動(含子活動)時,會這樣運作:
db.tripsDao.getAllTrips()
:1 次查詢。getActivitiesForTrip
查詢。getChildActivitiesForActivity
查詢。假設有 5 個行程,每個行程有 3 個活動,每個活動有 2 個子活動,總共會有 1 + 5 + (5 * 3) = 21 次查詢!在數據量大時,這會嚴重影響效能。
因此,我們應該使用 JOIN 查詢,一次性撈取所有資料。
JOIN 就像是在資料庫中進行「表格合併」,將 TripsTable
、ActivitiesTable
和 ChildActivitiesTable
這三個表格,根據它們的外來鍵(tripId
和 activityId
),合併成一個巨大的結果集。這樣一來,你只需執行一次查詢,就能獲得所有行程、活動及子活動的資料。
@DriftAccessor(tables: [TripsTable, ActivitiesTable, ChildActivitiesTable])
class TripsDao extends DatabaseAccessor<AppDatabase> with _$TripsDaoMixin {
TripsDao(super.attachedDatabase);
// 一個一次性撈取所有關聯資料的函式
Future<List<Trip>> getTripsWithActivitiesAndChildren() async {
// 進行兩次 leftOuterJoin,將三個表格合併
final query = select(tripsTable).join([
leftOuterJoin(
activitiesTable,
activitiesTable.tripId.equalsExp(tripsTable.id),
),
leftOuterJoin(
childActivitiesTable,
childActivitiesTable.activityId.equalsExp(activitiesTable.id),
),
]);
// 執行查詢並取得原始結果
final result = await query.get();
// 略
}
}
// trips_dao.dart
Future<void> updateTrip(Trip trip) async {
final entry = TripsTableCompanion(
id: Value(trip.id),
name: Value(trip.title),
);
final query = update(tripsTable)..where((tbl) => tbl.id.equals(trip.id));
await query.write(entry);
}
// trip_provider.dart
await db.activitiesDao.updateActivity(updatedActivity);
Value()
是一個 Drift 特有的包裝器,它告訴 Drift,這個欄位是明確要被更新的。如果傳入的值可能是 null
,也需要被這個包裝器包裝起來。
原先的刪除寫法只會從 tripsTable
中移除資料,而留在 activitiesTable
和 childActivitiesTable
中的資料,則會變成孤兒資料(Orphaned Data)。
為了避免這種情況,我們應該使用級聯刪除(Cascading Deletes)。在定義表格時,告訴 Drift 每個外來鍵(Foreign Key)在父資料被刪除時該如何處理。
我們可以在 activitiesTable
和 childActivitiesTable
的定義中,加入 onDelete: KeyAction.cascade
。
class ActivitiesTable extends Table {
// 外來鍵,連結到 Trips 表格的 id
IntColumn get tripId => integer().references(
TripsTable,
#id,
// 當父資料被刪除時,也一併刪除此活動
onDelete: KeyAction.cascade,
)();
// 略
}
class ChildActivitiesTable extends Table {
// 外來鍵,指向其父活動的 id
IntColumn get activityId => integer().references(
ActivitiesTable,
#id, // 當父資料被刪除時,也一併刪除此活動
onDelete: KeyAction.cascade,
)();
// 略
}
KeyAction
提供了多種選擇:
KeyAction.cascade
:當父資料被刪除時,一併刪除所有子資料。KeyAction.restrict
:如果存在子資料,則阻止父資料被刪除。KeyAction.setNull
:將所有子資料的外來鍵設為 NULL
。KeyAction.setDefault
:將所有子資料的外來鍵設為預設值。這兩天我從無到有摸懂了這個套件,從建置環境到實作。過去我習慣想到什麼就問什麼,但單純使用 Gemini Flash 2.5 時,我的詢問很容易讓它偏離正軌,花費額外時間拉回。
而「職涯導師」模式的 Gemini 則會把我拉回主線,在回答我的延伸問題後,會詢問我是想繼續課程,還是針對疑問進行更深入的探討。這種引導方式對我幫助很大。它也會在每段對話結束時,主動詢問是否還有其他問題,或是否可以進入下一章節,避免了單向的資訊轟炸。
在回答上,大方向沒有問題,但過程中提供的程式碼並非百分之百正確,仍需花點心思調整。如果它陷入鬼打牆,我會建議直接查閱官方文件。
總體來說,它的回應速度快,準確度也高。我認為未來若能一併將 GitHub 專案連結納入他一開始的參考資料,效果會更好。整體使用體驗很不錯,未來若有新套件需要了解,它將會是我的優先選擇。