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>
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
// ... (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 方法)
}
關鍵變更:
我們移除了
.fallbackToDestructiveMigration()。我們加入了
.addMigrations(MIGRATION_1_2, MIGRATION_2_3),將我們定義好的遷移物件註冊到 Room 中。
現在,當 Room 偵測到使用者手機中的資料庫是 v1,而 App 的程式碼是 v3 時,它會聰明地依序執行 MIGRATION_1_2 和 MIGRATION_2_3,安全地將資料庫升級到最新版本,同時保留所有現有資料。
第三步:(關鍵步驟) 測試你的遷移
未經測試的遷移是危險的。Android 提供了專門的測試工具來驗證我們的遷移邏輯是否正確。
A. 加入測試依賴
在 app/build.gradle.kts 中,確保 dependencies 區塊有這個依賴:
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>
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 的相關事宜。無論你的下一步是什麼,我都隨時準備好繼續提供協助!
再次恭喜你,完成了這個卓越的專案!