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

CRUD 操作

灵动桌面框架的数据库 CRUD 操作封装在 Database 层(src-tauri/src/database/mod.rs),以内置的 app_config 表为例。

所有方法都先拿锁再操作;锁中毒时转成 AppError::Custom,不使用 unwrap():

let conn = self.conn.lock().map_err(|e| AppError::Custom(e.to_string()))?;

查询所有

impl Database {
    /// 获取所有配置(排除已软删除的)
    pub fn get_all_config(&self) -> Result<Vec<AppConfig>, AppError> {
        let conn = self.conn.lock().map_err(|e| AppError::Custom(e.to_string()))?;
        let mut stmt = conn.prepare(
            "SELECT key, value FROM app_config WHERE deleted_at IS NULL ORDER BY key",
        )?;
        let configs = stmt
            .query_map([], |row| {
                Ok(AppConfig {
                    key: row.get(0)?,
                    value: row.get(1)?,
                })
            })?
            .collect::<Result<Vec<_>, _>>()?;
        Ok(configs)
    }
}

AppConfig 定义在 src-tauri/src/models/mod.rs,只有 key 和 value 两个字段。

查询单条

/// 获取单个配置(排除已软删除的)
pub fn get_config(&self, key: &str) -> Result<Option<String>, AppError> {
    let conn = self.conn.lock().map_err(|e| AppError::Custom(e.to_string()))?;
    let mut stmt = conn.prepare(
        "SELECT value FROM app_config WHERE key = ?1 AND deleted_at IS NULL",
    )?;
    let result = stmt
        .query_row([key], |row| row.get::<_, String>(0))
        .ok();
    Ok(result)
}

查不到时返回 None,由 Service 层转成 AppError::NotFound(前端收到错误码 NOT_FOUND)。

新增/更新(Upsert)

/// 设置配置(upsert)
pub fn set_config(&self, key: &str, value: &str) -> Result<(), AppError> {
    let conn = self.conn.lock().map_err(|e| AppError::Custom(e.to_string()))?;
    conn.execute(
        "INSERT INTO app_config (key, value, updated_at)
         VALUES (?1, ?2, datetime('now', 'localtime'))
         ON CONFLICT(key) DO UPDATE SET
           value = excluded.value,
           updated_at = excluded.updated_at",
        [key, value],
    )?;
    Ok(())
}

删除(软删除)

默认的删除是软删除:只写入 deleted_at,数据仍保留在库中。

/// 软删除配置(设置 deleted_at 而非物理删除)
pub fn delete_config(&self, key: &str) -> Result<bool, AppError> {
    let conn = self.conn.lock().map_err(|e| AppError::Custom(e.to_string()))?;
    let affected = conn.execute(
        "UPDATE app_config SET deleted_at = datetime('now', 'localtime') WHERE key = ?1 AND deleted_at IS NULL",
        [key],
    )?;
    Ok(affected > 0)
}

返回值表示是否真的删掉了一条;Service 层在返回 false 时报 NOT_FOUND。

此外 Database 层还提供了软删除三件套的另外两个方法,默认没有接成 Command,业务需要时再自行暴露:

方法SQL说明
hard_delete_config(key)DELETE FROM app_config WHERE key = ?1物理删除,不可恢复
restore_config(key)UPDATE app_config SET deleted_at = NULL WHERE key = ?1 AND deleted_at IS NOT NULL恢复已软删除的配置

TIP

软删除后再对同一个 key 调用 set_config,会走 ON CONFLICT 分支更新 value,但不会清空 deleted_at——这条配置依然处于已删除状态。需要"删了再加回来"时,先调用 restore_config。

完整调用链

前端: configApi.delete('theme')                         // src/lib/api/config.ts
  → invoke("delete_config", { key })
    → Command: commands::config::delete_config(state, key)
      → Service: ConfigService::delete(&state.db, &key)
        → Database: delete_config(&key)
          → SQL: UPDATE app_config SET deleted_at = ... WHERE key = 'theme' AND deleted_at IS NULL

前端 configApi 提供 getAll / get / set / delete 四个方法,分别对应 get_all_config / get_config / set_config / delete_config 四个 Command。

参数绑定

安全

始终使用参数化查询(?1, ?2),禁止字符串拼接 SQL,防止 SQL 注入。

// ✅ 正确:参数绑定
conn.execute("DELETE FROM t WHERE key = ?1", [key])?;

// ❌ 错误:字符串拼接
conn.execute(&format!("DELETE FROM t WHERE key = '{}'", key), [])?;