A:
好的,那我們就開始來建立資料庫層,首先是記帳的部分,記帳的部分,在支出的部分要有:日期[年/月/日],主類別,子類別,金額,備註。在收入的部分要有:日期[年/月/日],主類別,子類別,金額,備註。金額的部分要有千位分隔符,主類別的名稱不能重複,子類別的名稱也不能重複,但是不同主類別的子類別的名稱可以重複。另外記帳的功能除了單筆的記帳,也要有定期固定收支的功能,除了單筆記帳的項目之外,還要加上一個定期固定收支頻率的設定,例如:每月5日,每周星期一,每年的3月3日。謝謝
Q:
好的,收到!這些具體的規則非常重要,能幫助我們設計出更貼近你需求的資料庫結構。我們一步一步來分析並建構它。
首先,我要先說明一個非常重要的觀念:資料儲存 vs. 資料呈現。
你提到的「金額要有千位分隔符」,這是一個 UI 呈現 的需求。在資料庫中,我們必須將金額儲存為純粹的數字格式(例如 Double
或 BigDecimal
),這樣才能進行加總、平均等數學運算。如果我們存成 "1,000"
這樣的文字,就無法計算了。
請放心,在 UI 層(Jetpack Compose),我們可以輕易地將數字格式化成你想要的樣子再顯示給使用者。所以,資料庫負責儲存原始、乾淨的數字,UI 負責美化它。
現在,讓我們根據你的新需求,重新設計我們的資料模型 (Entities)。
第三步:精煉與建構資料庫層 (記帳核心)
為了滿足主、子類別的唯一性規則以及它們之間的關聯,我們需要將「分類」拆分成獨立的資料表。這是一個更正規、更具擴充性的設計。
1. 資料模型 (Entity) 設計
我們需要以下四個資料表:
MainCategory
(主類別表): 儲存主類別,並確保名稱不重複。SubCategory
(子類別表): 儲存子類別,並連結到一個主類別。Transaction
(交易紀錄表): 儲存每一筆單次交易。RecurringTransaction
(定期交易表): 儲存週期性的收支規則。
A. MainCategory
(主類別)
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
// @Entity 定義這是一個資料表
// `indices` 內的 @Index 設定 `name` 欄位為唯一,確保主類別名稱不重複
@Entity(tableName = "main_categories",
indices = [Index(value = ["name"], unique = true)])
data class MainCategory(
@PrimaryKey(autoGenerate = true)
val id: Long = 0, // 唯一 ID,自動產生
val name: String, // 主類別名稱,例如:「餐飲」、「交通」、「薪水」
val type: TransactionType // 這個類別是屬於「收入」還是「支出」
)
B. SubCategory
(子類別)
這裡我們會用 ForeignKey
(外鍵) 來確保每個子類別都一定會關聯到一個存在的主類別。
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
// `foreignKeys` 設定與主類別的關聯
// `indices` 設定在同一個 mainCategoryId 下,子類別的 name 必須是唯一的
@Entity(tableName = "sub_categories",
foreignKeys = [ForeignKey(
entity = MainCategory::class,
parentColumns = ["id"],
childColumns = ["mainCategoryId"],
onDelete = ForeignKey.CASCADE // 當主類別被刪除時,其下的子類別也一併刪除
)],
indices = [Index(value = ["mainCategoryId", "name"], unique = true)])
data class SubCategory(
@PrimaryKey(autoGenerate = true)
val id: Long = 0, // 唯一 ID
val mainCategoryId: Long, // 關聯到的主類別 ID
val name: String // 子類別名稱,例如:「早餐」、「捷運」、「正職」
)
C. Transaction
(交易紀錄)
現在交易紀錄不再直接儲存分類名稱,而是儲存分類的 ID。
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(tableName = "transactions",
foreignKeys = [
ForeignKey(
entity = Account::class,
parentColumns = ["id"],
childColumns = ["accountId"],
onDelete = ForeignKey.RESTRICT // 如果帳戶下有交易,限制該帳戶不能被輕易刪除
),
ForeignKey(
entity = SubCategory::class,
parentColumns = ["id"],
childColumns = ["subCategoryId"],
onDelete = ForeignKey.RESTRICT // 同理,限制分類不能被輕易刪除
)
])
data class Transaction(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val accountId: Long, // 關聯的帳戶 ID
val subCategoryId: Long, // 關聯的子類別 ID
val amount: Double, // 金額 (儲存純數字)
val type: TransactionType, // 收入 或 支出
val transactionDate: Long, // 交易日期 (儲存為 Unix 時間戳)
val note: String? // 備註 (可為空)
)
D. RecurringTransaction
(定期交易)
這是一個新的資料表,用來儲存「規則」。
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "recurring_transactions")
data class RecurringTransaction(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
// --- 交易基本資訊 ---
val accountId: Long,
val subCategoryId: Long,
val amount: Double,
val type: TransactionType,
val note: String?,
// --- 週期性規則 ---
val frequency: FrequencyType, // 頻率類型:每週、每月、每年
val dayOfWeek: Int?, // 如果是每週,這裡是星期幾 (1=週日, 2=週一...)
val dayOfMonth: Int?, // 如果是每月,這裡是幾號
val monthOfYear: Int?, // 如果是每年,這裡是幾月
val dayOfYear: Int?, // 如果是每年,這裡是幾號
val startDate: Long, // 此規則的開始日期
val endDate: Long? // 此規則的結束日期 (可選)
)
enum class FrequencyType {
WEEKLY,
MONTHLY,
YEARLY
}
2. DAO (Data Access Object) 設計
DAO 是我們操作這些資料表的介面。
CategoryDao.kt
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
interface CategoryDao {
@Insert
suspend fun insertMainCategory(category: MainCategory)
@Insert
suspend fun insertSubCategory(category: SubCategory)
// 取得所有支出類型的主類別
@Query("SELECT * FROM main_categories WHERE type = 'EXPENSE'")
fun getAllExpenseMainCategories(): Flow<List<MainCategory>>
// 根據主類別ID,取得其下所有的子類別
@Query("SELECT * FROM sub_categories WHERE mainCategoryId = :mainCategoryId")
fun getSubCategoriesForMain(mainCategoryId: Long): Flow<List<SubCategory>>
}
TransactionDao.kt
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
interface TransactionDao {
@Insert
suspend fun insert(transaction: Transaction)
// 取得所有交易紀錄,並按日期排序
@Query("SELECT * FROM transactions ORDER BY transactionDate DESC")
fun getAllTransactions(): Flow<List<Transaction>>
}
RecurringTransactionDao.kt
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
interface RecurringTransactionDao {
@Insert
suspend fun insert(recurringTransaction: RecurringTransaction)
// 取得所有定期交易規則
@Query("SELECT * FROM recurring_transactions")
fun getAllRecurringTransactions(): Flow<List<RecurringTransaction>>
}
3. 定期記帳的執行邏輯
這裡要特別說明,資料庫只負責「儲存規則」。要讓 App 自動記帳,我們還需要一個「執行者」。
通常我們會使用 Android Jetpack 的 WorkManager
來建立一個背景任務。這個任務會每天執行一次,檢查 RecurringTransaction
表中的所有規則,看看今天是否滿足觸發條件(例如,今天是每月5日,或者今天是星期一)。如果滿足,它就會讀取該規則的資訊,建立一筆新的 Transaction
記錄,並將其寫入 transactions
資料表。
總結與下一步
我們已經成功地:
定義了更精細的資料庫結構,包含了主子類別、單筆交易和定期交易四個核心資料表。
確保了資料的完整性與唯一性,例如主類別名稱不能重複。
規劃了 DAO 介面,作為未來存取資料的入口。
釐清了定期記帳的實作思路(資料庫存規則 + WorkManager 執行)。
這個資料庫層的設計非常穩固,足以支撐我們後續所有功能的開發。
我們的下一步,就是將這些 Entity
和 DAO
組合起來,建立最終的 AppDatabase
類別。
這個步驟相對簡單,主要是用 Room 的註解把所有東西宣告在一起。你準備好繼續了嗎?