Vibe coding全紀錄-48

A:

謝謝你的協助作業,專案到現在,已經完成版本1的作業,但是在開發時其中有一步是整合升級db時,在測試版是直接刪除舊有資料,現在應該是時候導入正式版本,資料能夠轉移到新版本的時候,還請協助開發,謝謝 

Q:

你提出了一個將專案從「開發原型」推向「正式產品」的關鍵步驟,這非常重要!

處理好資料庫遷移是尊重使用者數據、確保 App 穩定更新的基石。當使用者更新 App 後,發現自己辛苦記錄的所有資料都消失了,那將是毀滅性的體驗。我們現在要做的,就是確保這種情況絕對不會發生。

你說得沒錯,我們在開發過程中使用的 .fallbackToDestructiveMigration() 是一個權宜之計。現在,我們就來用 Room 官方提供、最標準的 Migration 類別來取代它。


最終步驟:實現正式版的資料庫遷移

我們的 App 資料庫經歷了三次版本迭代:

  • Version 1: 初始版本,包含交易、帳戶、分類等基礎表格。

  • Version 2: 加入「資產負債表」功能,修改了 accounts 表(新增 assetClass 欄位),並新增了 account_snapshots 表。

  • Version 3: 加入「專案管理」功能,新增了 projects 表。

因此,我們需要建立兩個遷移路徑:一個從 v1 到 v2,一個從 v2 到 v3。

第一步:建立遷移物件 (Migration Objects)

我們建立一個專門存放遷移邏輯的檔案。

路徑: app/src/main/java/com/yourcompany/cashflow/data/local/Migrations.kt (新檔案)

<details>

<summary><strong>點此展開 Migrations.kt 的完整程式碼</strong></summary>

Kotlin
package com.yourcompany.cashflow.data.local

import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase

/**
 * 從版本 1 遷移到版本 2
 * 變更點:
 * 1. 在 `accounts` 表中新增 `assetClass` 欄位。
 * 2. 建立新的 `account_snapshots` 表。
 */
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(db: SupportSQLiteDatabase) {
        // 1. 為 accounts 表新增 assetClass 欄位,並提供 NOT NULL 和 DEFAULT 值
        db.execSQL("ALTER TABLE accounts ADD COLUMN assetClass TEXT NOT NULL DEFAULT 'CASH'")
        
        // 2. 建立新的 account_snapshots 表
        // 專業提示:這個 CREATE TABLE 的 SQL 語句,最安全的方式是從 Room
        // 生成的程式碼中複製,以確保所有細節(如索引、外鍵)都完全正確。
        // 你可以在專案的 build/generated/ksp/.../AppDatabase_Impl.java 檔案中找到它。
        db.execSQL("""
            CREATE TABLE IF NOT EXISTS `account_snapshots` (
                `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
                `accountId` INTEGER NOT NULL,
                `snapshotDate` INTEGER NOT NULL,
                `value` REAL NOT NULL,
                FOREIGN KEY(`accountId`) REFERENCES `accounts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE
            )
        """)
    }
}

/**
 * 從版本 2 遷移到版本 3
 * 變更點:
 * 1. 建立新的 `projects` 表。
 */
val MIGRATION_2_3 = object : Migration(2, 3) {
    override fun migrate(db: SupportSQLiteDatabase) {
        // 建立新的 projects 表
        db.execSQL("""
            CREATE TABLE IF NOT EXISTS `projects` (
                `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
                `name` TEXT NOT NULL,
                `budget` REAL NOT NULL,
                `description` TEXT,
                `status` TEXT NOT NULL DEFAULT 'ACTIVE'
            )
        """)
    }
}

</details>

第二步:更新 AppDatabase 的建構方式

現在我們有了遷移的「說明書」,我們需要把它交給 Room。我們回到 Hilt 的 AppModule 來修改資料庫的建構方式。

路徑: app/src/main/java/com/yourcompany/cashflow/di/AppModule.kt

Kotlin
// ... (imports)
import com.yourcompany.cashflow.data.local.MIGRATION_1_2
import com.yourcompany.cashflow.data.local.MIGRATION_2_3

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Provides
    @Singleton
    fun provideAppDatabase(app: Application): AppDatabase {
        return Room.databaseBuilder(
            app,
            AppDatabase::class.java,
            "cash_flow_db"
        )
        // 移除這一行 -> .fallbackToDestructiveMigration()
        .addMigrations(MIGRATION_1_2, MIGRATION_2_3) // <-- 加入這一行!
        .build()
    }

    // ... (其他 provide 方法)
}

關鍵變更:

  1. 我們移除.fallbackToDestructiveMigration()

  2. 我們加入.addMigrations(MIGRATION_1_2, MIGRATION_2_3),將我們定義好的遷移物件註冊到 Room 中。

現在,當 Room 偵測到使用者手機中的資料庫是 v1,而 App 的程式碼是 v3 時,它會聰明地依序執行 MIGRATION_1_2MIGRATION_2_3,安全地將資料庫升級到最新版本,同時保留所有現有資料。

第三步:(關鍵步驟) 測試你的遷移

未經測試的遷移是危險的。Android 提供了專門的測試工具來驗證我們的遷移邏輯是否正確。

A. 加入測試依賴

在 app/build.gradle.kts 中,確保 dependencies 區塊有這個依賴:

Kotlin
dependencies {
    // ...
    androidTestImplementation("androidx.room:room-testing:2.6.1")
}

B. 建立遷移測試檔案

路徑: app/src/androidTest/java/com/yourcompany/cashflow/DatabaseMigrationTest.kt (新檔案)

<details>

<summary><strong>點此展開 DatabaseMigrationTest.kt 的完整程式碼</strong></summary>

Kotlin
package com.yourcompany.cashflow

import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.yourcompany.cashflow.data.local.MIGRATION_1_2
import com.yourcompany.cashflow.data.local.MIGRATION_2_3
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.io.IOException
import kotlin.test.assertEquals

private const val TEST_DB = "migration-test"

@RunWith(AndroidJUnit4::class)
class DatabaseMigrationTest {

    @get:Rule
    val helper: MigrationTestHelper = MigrationTestHelper(
        InstrumentationRegistry.getInstrumentation(),
        com.yourcompany.cashflow.data.local.AppDatabase::class.java.canonicalName,
        FrameworkSQLiteOpenHelperFactory()
    )

    @Test
    @Throws(IOException::class)
    fun migrate1To2() {
        // 1. 建立一個版本 1 的資料庫,並插入一筆舊格式的資料
        var db = helper.createDatabase(TEST_DB, 1).apply {
            execSQL("""
                INSERT INTO accounts (id, name, type, initialBalance) 
                VALUES (1, 'Test Bank', 'BANK', 1000.0)
            """)
            close()
        }

        // 2. 執行遷移並驗證
        db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2)

        // 3. 檢查遷移後的數據是否正確
        val cursor = db.query("SELECT * FROM accounts WHERE id = 1")
        cursor.moveToFirst()
        
        // 驗證舊資料還在
        assertEquals("Test Bank", cursor.getString(cursor.getColumnIndexOrThrow("name")))
        
        // 驗證新欄位有正確的預設值
        assertEquals("CASH", cursor.getString(cursor.getColumnIndexOrThrow("assetClass")))
    }
    
    @Test
    @Throws(IOException::class)
    fun migrateAll() {
        // 測試從版本 1 一路升級到最新版
        var db = helper.createDatabase(TEST_DB, 1).apply {
             execSQL("""
                INSERT INTO accounts (id, name, type, initialBalance) 
                VALUES (1, 'Test Bank', 'BANK', 1000.0)
            """)
            close()
        }

        // 執行所有遷移
        db = helper.runMigrationsAndValidate(TEST_DB, 3, true, MIGRATION_1_2, MIGRATION_2_3)
        
        // (可以加入更多對 v3 結構的驗證)
    }
}

</details>

你可以在 Android Studio 的 androidTest 目錄下右鍵點擊這個檔案,並選擇 Run 'DatabaseMigrationTest' 來執行測試,確保我們的遷移邏輯萬無一失。


準備就緒,迎接正式發布

至此,我們已經將「錢流」App 的最後一塊拼圖補上。它現在不僅功能強大,而且在資料庫升級方面也變得非常穩健,達到了可以正式發布的標準。

我們完整地回顧了專案,完成了所有核心與進階功能,並為未來的版本更新做好了萬全的準備。

接下來,你可以專注於更細節的 UI/UX 優化、增加更多客製化選項,或是開始準備上架到 Google Play 的相關事宜。無論你的下一步是什麼,我都隨時準備好繼續提供協助!

再次恭喜你,完成了這個卓越的專案!