準備加新功能時,我發現一個問題。
SQLDelight 的文件提到 migration,但語焉不詳。我的專案要怎麼處理資料庫版本更新?
看了看現有程式碼:
// 直接 create table,沒有版本控制
GrimoDatabase.Schema.create(driver)
這樣不行啊。用戶更新應用程式時,資料庫結構變了怎麼辦?
「請以 ARS 角色,研究 SQLDelight 2.0 最新的 migration 最佳實踐。」
ARS 的研究報告:
## 研究報告:SQLDelight Migration 方案
### 執行摘要
- SQLDelight 2.0 新增 deriveSchemaFromMigrations
- 使用 PRAGMA user_version 管理版本
- .sqm 檔案專門處理 migration
### 關鍵發現
1. **deriveSchemaFromMigrations** - Schema 從 .sqm 產生
2. **PRAGMA user_version** - SQLite 內建版本控制
3. **自動 transaction** - migration 自動包裝
### 業界實踐
- Cash App:100+ migrations,只用 30 行 helper
- Signal Desktop:用 PRAGMA user_version
「什麼是 .sqm?跟 .sq 有什麼差別?」我問。
「Claude Code,根據 ARS 的研究,幫我理解 deriveSchemaFromMigrations 怎麼運作。」
Claude Code 解釋:「這是 SQLDelight 2.0 的新功能。設定後,資料庫 schema 完全從 migration 檔案產生,而不是從 .sq 檔案。」
「那版本控制呢?」我問。
「SQLite 有內建的 PRAGMA user_version,」Claude Code 說,「不需要額外的版本表。」
PRAGMA user_version?我從來沒用過。
「怎麼用 PRAGMA user_version?」我自己試試看:
-- 查詢目前版本
PRAGMA user_version;
-- 結果:0
-- 設定版本
PRAGMA user_version = 1;
-- 再次查詢
PRAGMA user_version;
-- 結果:1
原來這麼簡單!SQLite 內建的功能。
參考文件和測試後,我寫了簡單的 MigrationHelper:
object MigrationHelper {
suspend fun migrateIfNeeded(driver: SqlDriver) {
val currentVersion = getCurrentVersion(driver)
val targetVersion = GrimoDatabase.Schema.version
when {
currentVersion == 0L -> {
// 新資料庫,直接建立
GrimoDatabase.Schema.create(driver).await()
}
currentVersion < targetVersion -> {
// 需要升級
GrimoDatabase.Schema.migrate(
driver, currentVersion, targetVersion
).await()
}
}
setVersion(driver, targetVersion)
}
private fun getCurrentVersion(driver: SqlDriver): Long =
driver.executeQuery(
null, "PRAGMA user_version",
{ cursor -> QueryResult.Value(cursor.getLong(0) ?: 0L) },
0
).value
private fun setVersion(driver: SqlDriver, version: Long) {
driver.execute(null, "PRAGMA user_version = $version", 0)
}
}
30 行左右,簡潔明瞭。
看了文件後,我修改 build.gradle.kts:
sqldelight {
databases {
create("GrimoDatabase") {
packageName.set("io.github.grimostudio.grimo.db")
// 從 migration 檔案產生 schema
deriveSchemaFromMigrations.set(true)
// 其他設定
generateAsync.set(true) // 支援協程
verifyMigrations.set(true) // 編譯時驗證
}
}
}
「deriveSchemaFromMigrations 是關鍵,」我理解了,「設定後,不用在 .sq 檔案寫 CREATE TABLE。」
專案結構變成:
sqldelight/
├── migrations/
│ ├── 1.sqm # 初始 schema
│ └── 2.sqm # 新增功能
└── io/.../grimo/
└── Project.sq # 只有查詢
第一個 migration (1.sqm):
-- 初始資料庫結構
CREATE TABLE Project (
id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
createdAt INTEGER NOT NULL,
updatedAt INTEGER NOT NULL
);
第二個 migration (2.sqm) 加入 EventStore:
-- 支援 CloudEvents 和 Event Sourcing
CREATE TABLE IF NOT EXISTS event_store (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
stream_id TEXT NOT NULL,
cloud_event_id TEXT UNIQUE NOT NULL,
-- ... 更多欄位
);
清空資料庫,重新執行:
資料庫版本: 0
執行 Schema.create()
設定版本為 2
成功!
把 user_version 改成 1,再執行:
資料庫版本: 1
執行 Schema.migrate(1, 2)
設定版本為 2
也成功了!
一開始我把 migration 檔案命名成 001.sqm、002.sqm。
結果編譯錯誤:
Expected migration file to be named <version>.sqm
「噢,要用數字當檔名,」我改成 1.sqm、2.sqm,問題解決。
「Claude Code,migration 檔案是依序執行嗎?」
「對,」Claude Code 回答,「從當前版本執行到目標版本。比如從版本 1 升級到版本 3,會依序執行 2.sqm 和 3.sqm。」
「那如果中間有錯誤呢?」
「SQLDelight 會自動包在 transaction 裡,失敗就回滾。」
原來如此,不用自己處理 transaction。
「有幾個最佳實踐,」Claude Code 建議:
我記下這些原則。
我的專案第二個 migration 比較複雜,加入 EventStore 支援 CloudEvents:
-- Migration 2: EventStore for CloudEvents and Event Sourcing
CREATE TABLE IF NOT EXISTS event_store (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
stream_id TEXT NOT NULL,
stream_version INTEGER NOT NULL,
cloud_event_id TEXT UNIQUE NOT NULL,
cloud_event_source TEXT NOT NULL,
cloud_event_type TEXT NOT NULL,
-- ... 更多欄位
UNIQUE(stream_id, stream_version),
CHECK(stream_version > 0)
);
-- 建立索引提升查詢效能
CREATE INDEX IF NOT EXISTS idx_event_store_stream
ON event_store(stream_id, stream_version);
這個 migration 有 87 行,但很清楚:建立事件存儲系統。
原本想寫一個「完美」的 migration 系統,支援:
結果發現 SQLDelight + PRAGMA user_version 就夠了。
剛開始不懂 deriveSchemaFromMigrations,查了文件、看了範例、問了 Claude Code,慢慢理解。
現在回頭看,其實概念很簡單:
SQLDelight 團隊已經想過這些問題,提供了優雅的解決方案。
我不需要重新發明輪子,專注在業務邏輯就好。
今天研究了 SQLDelight 的 migration 機制,從一開始的困惑到最後理解。
關鍵發現:
Claude Code 在這個過程中扮演研究助手的角色,幫我理解概念、分析文件、提供最佳實踐建議。
最大的收穫是學會相信框架。SQLDelight 提供的功能已經很完善,不需要過度設計。
明天繼續探索其他架構優化的可能性。
「最好的架構是看不見的架構。」
關於作者:Sam,一人公司創辦人。正在打造 Grimo,智能任務管理平台。
專案連結:GitHub - grimostudio