跳到正文
灵动桌面框架
Esc
本栏目菜单

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(())
}

添加新迁移

当需要修改数据库结构时:

  1. 递增 SCHEMA_VERSION
  2. 添加新的迁移函数,函数末尾调用 set_version(conn, 新版本号)
  3. 在 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 写法,靠版本号保证只执行一次
  • 已发布的迁移不再修改 — 用户机器上已经执行过的迁移函数改了也不会重跑,结构调整一律新增一个版本