iT邦幫忙

2025 iThome 鐵人賽

DAY 17
0
Mobile Development

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

Day 17 - Drift CRUD 入門:跟著 Gemini 玩轉資料庫

  • 分享至 

  • xImage
  •  

今天我持續透過 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)的寫法看似多包了一層,但它帶來了幾個顯著的好處:

  • 單一職責: 資料存取邏輯集中管理,讓 provider 只負責業務邏輯。
  • 可維護性高: 未來若需調整插入邏輯,只需修改 DAO,無需大範圍變動呼叫端程式碼。
  • 可測試性更好: DAO 方法可以單獨進行單元測試,確保資料庫操作的正確性。
  • 結構更清晰: 隨著專案規模成長,DAO 能讓程式碼層次更清楚,避免資料操作邏輯散落各處。

撈取資料:基本查詢

撈取行程列表(不含活動)

// 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 中執行資料轉換?

  • 解耦: 你的應用程式邏輯不會直接依賴於 Drift 生成的類別。未來即使更換資料庫套件,你只需修改 DAO 內部的轉換邏輯,而應用程式程式碼則能保持不變。
  • 單一事實來源: Trip 模型是你應用程式中使用的標準資料結構。讓 DAO 負責將資料庫格式轉換為應用程式格式,可以確保所有層級都使用一致的資料模型。
  • 封裝邏輯: 你的 Trip 模型可能包含額外的業務邏輯。在轉換的過程中,你可以一併處理這些邏輯。

撈取資料:N+1 查詢問題與 JOIN 查詢

雖然我們學會了基本的資料查詢,但在處理複雜的關聯資料時,原先的寫法會引發一個嚴重的效能問題——N+1 查詢問題

這就像是你去超市購物,但每拿一樣東西就跑去結一次帳,重複多次。原先的寫法在撈取行程及其活動(含子活動)時,會這樣運作:

  • db.tripsDao.getAllTrips():1 次查詢。
  • 外層迴圈:N 個行程,執行 N 次 getActivitiesForTrip 查詢。
  • 內層迴圈:每個行程下的 M 個活動,再執行 M 次 getChildActivitiesForActivity 查詢。

假設有 5 個行程,每個行程有 3 個活動,每個活動有 2 個子活動,總共會有 1 + 5 + (5 * 3) = 21 次查詢!在數據量大時,這會嚴重影響效能。

因此,我們應該使用 JOIN 查詢,一次性撈取所有資料。

JOIN 就像是在資料庫中進行「表格合併」,將 TripsTableActivitiesTableChildActivitiesTable 這三個表格,根據它們的外來鍵(tripIdactivityId),合併成一個巨大的結果集。這樣一來,你只需執行一次查詢,就能獲得所有行程、活動及子活動的資料。

@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 中移除資料,而留在 activitiesTablechildActivitiesTable 中的資料,則會變成孤兒資料(Orphaned Data)

為了避免這種情況,我們應該使用級聯刪除(Cascading Deletes)。在定義表格時,告訴 Drift 每個外來鍵(Foreign Key)在父資料被刪除時該如何處理。

我們可以在 activitiesTablechildActivitiesTable 的定義中,加入 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 職涯導師使用心得

這兩天我從無到有摸懂了這個套件,從建置環境到實作。過去我習慣想到什麼就問什麼,但單純使用 Gemini Flash 2.5 時,我的詢問很容易讓它偏離正軌,花費額外時間拉回。

而「職涯導師」模式的 Gemini 則會把我拉回主線,在回答我的延伸問題後,會詢問我是想繼續課程,還是針對疑問進行更深入的探討。這種引導方式對我幫助很大。它也會在每段對話結束時,主動詢問是否還有其他問題,或是否可以進入下一章節,避免了單向的資訊轟炸。

在回答上,大方向沒有問題,但過程中提供的程式碼並非百分之百正確,仍需花點心思調整。如果它陷入鬼打牆,我會建議直接查閱官方文件。

總體來說,它的回應速度快,準確度也高。我認為未來若能一併將 GitHub 專案連結納入他一開始的參考資料,效果會更好。整體使用體驗很不錯,未來若有新套件需要了解,它將會是我的優先選擇。


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

尚未有邦友留言

立即登入留言