Schema 迁移
灵动桌面框架使用 SQLite 的 PRAGMA user_version 实现简单可靠的 Schema 版本管理。迁移在 Database::init() 打开连接后自动执行,应用每次启动都会检查一次。
迁移机制
// src-tauri/src/database/schema.rs
/// 当前 Schema 版本
pub const SCHEMA_VERSION: i32 = 2;
/// 获取数据库版本
pub fn get_version(conn: &Connection) -> Result<i32, AppError> {
let version: i32 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
Ok(version)
}
/// 设置数据库版本
pub fn set_version(conn: &Connection, version: i32) -> Result<(), AppError> {
conn.pragma_update(None, "user_version", version)?;
Ok(())
}
/// 执行数据库迁移
pub fn migrate(conn: &Connection) -> Result<(), AppError> {
let mut version = get_version(conn)?;
if version > SCHEMA_VERSION {
return Err(AppError::Custom(format!(
"数据库版本({})高于应用支持的版本({}), 请升级应用",
version, SCHEMA_VERSION
)));
}
while version < SCHEMA_VERSION {
match version {
0 => migrate_v0_to_v1(conn)?,
1 => migrate_v1_to_v2(conn)?,
_ => {
return Err(AppError::Custom(format!(
"未知的数据库版本: {}",
version
)));
}
}
version = get_version(conn)?;
}
log::info!("数据库迁移完成, 当前版本: {}", version);
Ok(())
}
要点:
- 逐级升级 —
while循环每次只升一级(v0→v1→v2……),每个迁移函数结束时自己调用set_version写入新版本号,循环再重新读取 - 拒绝降级 — 数据库版本高于应用支持的版本(用户装回了旧版应用)时直接报错,避免旧代码误写新结构
- 新库与老库走同一条路 — 新安装的数据库
user_version为 0,会从 v0 一路迁到最新
框架内置的两个迁移:
/// v0 -> v1: 初始化表结构
fn migrate_v0_to_v1(conn: &Connection) -> Result<(), AppError> {
log::info!("数据库迁移: v0 -> v1");
conn.execute_batch(
"
CREATE TABLE IF NOT EXISTS app_config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
-- 插入默认配置
INSERT OR IGNORE INTO app_config (key, value) VALUES ('theme', 'light');
INSERT OR IGNORE INTO app_config (key, value) VALUES ('language', 'zh-CN');
INSERT OR IGNORE INTO app_config (key, value) VALUES ('sidebar_collapsed', 'false');
",
)?;
set_version(conn, 1)?;
Ok(())
}
/// v1 -> v2: 添加软删除支持
fn migrate_v1_to_v2(conn: &Connection) -> Result<(), AppError> {
log::info!("数据库迁移: v1 -> v2(软删除支持)");
conn.execute_batch(
"
-- app_config 添加 deleted_at 软删除字段
ALTER TABLE app_config ADD COLUMN deleted_at TEXT DEFAULT NULL;
",
)?;
set_version(conn, 2)?;
Ok(())
}
添加新迁移
当需要修改数据库结构时:
- 递增
SCHEMA_VERSION - 添加新的迁移函数,函数末尾调用
set_version(conn, 新版本号) - 在
migrate()的match中添加对应分支
pub const SCHEMA_VERSION: i32 = 3;
pub fn migrate(conn: &Connection) -> Result<(), AppError> {
// ……前略,与上面相同
while version < SCHEMA_VERSION {
match version {
0 => migrate_v0_to_v1(conn)?,
1 => migrate_v1_to_v2(conn)?,
2 => migrate_v2_to_v3(conn)?, // 新增
_ => { /* 未知版本报错 */ }
}
version = get_version(conn)?;
}
// ……
}
/// v2 -> v3: 新增 user_data 表
fn migrate_v2_to_v3(conn: &Connection) -> Result<(), AppError> {
log::info!("数据库迁移: v2 -> v3");
conn.execute_batch(
"
CREATE TABLE IF NOT EXISTS user_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
deleted_at TEXT DEFAULT NULL
);
",
)?;
set_version(conn, 3)?;
Ok(())
}
别忘了写版本号
迁移函数里漏掉 set_version,while 循环读到的版本号不变,会反复执行同一个迁移:只含 IF NOT EXISTS 语句的迁移会让启动卡死在循环里,含 ALTER TABLE ... ADD COLUMN 的迁移第二次执行会因列已存在而报错。
迁移原则
- 只增不删 — 新版本只添加表或列,不删除已有结构
- 向后兼容 — 老数据在新 Schema 下仍然可用
- 顺序执行 — 迁移按版本号逐级执行,不跳级
- 幂等性 — 建表用
IF NOT EXISTS、插入默认数据用INSERT OR IGNORE;ALTER TABLE ADD COLUMN没有IF NOT EXISTS写法,靠版本号保证只执行一次 - 已发布的迁移不再修改 — 用户机器上已经执行过的迁移函数改了也不会重跑,结构调整一律新增一个版本