A:
好的,可以開始第一步「修改資料庫結構」,謝謝
Q:
好的,我們立刻開始!
第一步「修改資料庫結構」是為新功能打下堅實地基的關鍵。我們將會新增一個資料表,並修改一個現有的資料表。
特別提醒: 這次的修改會變動到資料庫的「結構 (Schema)」。因此,我們需要處理一個重要的議題叫做「資料庫遷移 (Database Migration)」,我會在最後一步詳細說明如何用一個簡單的方式來處理它。
第十六步:修改資料庫結構
16.1 新增 AssetClass
枚舉 (Enum)
為了區分股票、債券等資產,我們先來定義這個新的分類。打開我們之前建立的 Enums.kt
檔案,在裡面加入 AssetClass
。
路徑: app/src/main/java/com/yourcompany/cashflow/data/local/entity/Enums.kt
package com.yourcompany.cashflow.data.local.entity
enum class AccountType { BANK, INVESTMENT, LIABILITY }
enum class TransactionType { INCOME, EXPENSE }
enum class FrequencyType { WEEKLY, MONTHLY, YEARLY }
// vvv-- 加入這個新的 Enum --vvv
enum class AssetClass {
CASH, // 現金 (銀行存款)
STOCK, // 股票
BOND, // 債券
OTHER // 其他資產
}
16.2 修改 Account
實體 (Entity)
現在,我們在 Account
資料表中加入 assetClass
這個新欄位。
路徑: app/src/main/java/com/yourcompany/cashflow/data/local/entity/Account.kt
package com.yourcompany.cashflow.data.local.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "accounts")
data class Account(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val name: String,
val type: AccountType,
// vvv-- 加入這個新欄位 --vvv
// 我們給它一個預設值,這樣 Room 在更新資料庫時,才知道要為舊資料填入什麼內容
@ColumnInfo(defaultValue = "CASH")
val assetClass: AssetClass,
val initialBalance: Double
)
16.3 建立 AccountSnapshot
實體 (Entity)
這是我們用來儲存每月資產快照的全新資料表。
路徑: app/src/main/java/com/yourcompany/cashflow/data/local/entity/AccountSnapshot.kt
(新檔案)
package com.yourcompany.cashflow.data.local.entity
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
tableName = "account_snapshots",
foreignKeys = [ForeignKey(
entity = Account::class,
parentColumns = ["id"],
childColumns = ["accountId"],
onDelete = ForeignKey.CASCADE // 如果帳戶被刪除,其所有快照也一併刪除
)]
)
data class AccountSnapshot(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val accountId: Long, // 關聯到哪個帳戶
val snapshotDate: Long, // 快照日期 (儲存時間戳)
val value: Double // 該日期的帳戶價值
)
16.4 建立 SnapshotDao
介面
我們需要一個新的 DAO 來操作新的 account_snapshots
資料表。
路徑: app/src/main/java/com/yourcompany/cashflow/data/local/dao/SnapshotDao.kt
(新檔案)
package com.yourcompany.cashflow.data.local.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import com.yourcompany.cashflow.data.local.entity.AccountSnapshot
@Dao
interface SnapshotDao {
// 我們先定義一個最基本的新增方法
// 之後會再回來加入更複雜的查詢方法
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(snapshot: AccountSnapshot)
}
16.5 更新 AppDatabase
並處理資料庫遷移
這是最關鍵的一步。我們需要讓 AppDatabase
知道有新的資料表和 DAO,並且處理版本升級。
A. 更新 AppDatabase.kt
// /data/local/AppDatabase.kt
package com.yourcompany.cashflow.data.local
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.yourcompany.cashflow.data.local.dao.*
import com.yourcompany.cashflow.data.local.entity.*
@Database(
entities = [
Account::class,
MainCategory::class,
SubCategory::class,
Transaction::class,
RecurringTransaction::class,
AccountSnapshot::class // <-- 1. 加入新的 Entity
],
version = 2, // <-- 2. 非常重要!將版本號從 1 改為 2
exportSchema = false
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun accountDao(): AccountDao
abstract fun categoryDao(): CategoryDao
abstract fun transactionDao(): TransactionDao
abstract fun recurringTransactionDao(): RecurringTransactionDao
abstract fun snapshotDao(): SnapshotDao // <-- 3. 加入新的 DAO
}
B. 處理資料庫遷移 (Database Migration)
由於我們修改了資料庫結構(增加了表格、修改了欄位)並提升了 version
,我們必須告訴 Room 如何從舊版 (v1) 過渡到新版 (v2)。若不處理,App 在更新後一啟動就會閃退。
在正式上架的 App 中,我們會寫詳細的遷移腳本來保留使用者資料。但在開發階段,有一個更簡單直接的方法:刪除舊資料庫,重新建立一個新的。
我們需要在 Hilt 的 AppModule
中設定它。
路徑: app/src/main/java/com/yourcompany/cashflow/di/AppModule.kt
// /di/AppModule.kt
...
object AppModule {
@Provides
@Singleton
fun provideAppDatabase(app: Application): AppDatabase {
return Room.databaseBuilder(
app,
AppDatabase::class.java,
"cash_flow_db"
)
.fallbackToDestructiveMigration() // <-- 加入這一行!
.build()
}
// ... 其他 provide 方法 ...
// vvv-- 為新的 DAO 加入 Provider --vvv
@Provides
@Singleton
fun provideSnapshotDao(db: AppDatabase): SnapshotDao = db.snapshotDao()
}
fallbackToDestructiveMigration()
這行程式碼的意思是:「如果需要版本升級,且沒有提供遷移腳本,就直接刪除所有資料並重建資料庫。」這在開發時非常方便,但請記住,發布 App 前需要換成正式的遷移方案,以免清除使用者的所有資料!
C. 更新 Converters.kt
最後,別忘了為我們新的 AssetClass enum 加入 TypeConverter。
// /data/local/Converters.kt
...
class Converters {
// ... 其他 Converters ...
@TypeConverter fun fromAssetClass(value: AssetClass) = value.name
@TypeConverter fun toAssetClass(value: String) = AssetClass.valueOf(value)
}
結構修改完成!
我們已經成功地更新了資料庫的藍圖,現在它已經準備好儲存我們資產負債表所需要的「每月快照」資料了。
資料庫的基礎建設已經完成。下一步,我們將建立 BalanceSheetViewModel
和對應的 Repository
,並在其中實作我們需要的複雜查詢邏輯,來為我們的圖表準備數據。準備好了嗎?