name: database-ops description: | Tauri 本地数据库操作技能,使用 rusqlite 进行 SQLite 数据库操作。
触发场景:
- 需要在桌面应用中持久化数据
- 需要使用 SQLite 数据库
- 需要设计本地数据表结构
- 需要执行 CRUD 数据库操作
触发词:数据库、SQLite、SQL、持久化、存储、表、查询、CRUD、数据
Tauri 本地数据库操作
核心架构
本项目采用 三层架构,数据库操作在最底层:
┌────────────────────────────────────────────┐
│ Commands (commands/*.rs) │ ← 前端调用
│ ↓ 调用 │
│ Services (services/*.rs) │ ← 业务逻辑
│ ↓ 调用 │
│ Database (database/mod.rs) │ ← 数据访问
│ - Mutex<Connection> │
│ - get_all_config() │
│ - set_config() │
└────────────────────────────────────────────┘
技术方案:rusqlite (推荐)
本项目使用 rusqlite(Rust 原生 SQLite 绑定),而非 tauri-plugin-sql。
为什么选择 rusqlite?
| 特性 | rusqlite | tauri-plugin-sql |
|---|---|---|
| 调用位置 | Rust 后端 | 前端 TypeScript |
| 类型安全 | 编译时检查 | 运行时检查 |
| 性能 | 无 IPC 开销 | 每次查询都走 IPC |
| 事务 | 原生支持 | 复杂场景支持差 |
| 安全性 | SQL 注入保护完善 | 依赖前端参数化 |
| 复杂查询 | 任意 SQL | 受插件 API 限制 |
安装依赖
# Cargo.toml
[dependencies]
rusqlite = { version = "0.31", features = ["bundled"] }
Database 结构设计
核心模式:Mutex<Connection>
// src-tauri/src/database/mod.rs
use std::sync::Mutex;
use rusqlite::Connection;
use crate::error::AppError;
pub struct Database {
conn: Mutex<Connection>, // 线程安全的连接
}
impl Database {
/// 初始化数据库(自动迁移)
pub fn init(db_path: &str) -> Result<Self, AppError> {
let conn = Connection::open(db_path)?;
// 启用 WAL 模式提升并发性能
conn.pragma_update(None, "journal_mode", "WAL")?;
// 执行 Schema 迁移
schema::migrate(&conn)?;
Ok(Self {
conn: Mutex::new(conn),
})
}
}
Schema 迁移(PRAGMA user_version)
迁移模式
// src-tauri/src/database/schema.rs
use rusqlite::Connection;
use crate::error::AppError;
pub fn migrate(conn: &Connection) -> Result<(), AppError> {
let version: u32 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
if version < 1 {
// ────────── 版本 1: 初始化 ──────────
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS app_config (
key TEXT PRIMARY KEY NOT NULL,
value TEXT NOT NULL,
created_at DATETIME DEFAULT (datetime('now', 'localtime')),
updated_at DATETIME DEFAULT (datetime('now', 'localtime'))
);"
)?;
conn.pragma_update(None, "user_version", 1)?;
}
if version < 2 {
// ────────── 版本 2: 新增表 ──────────
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL,
created_at DATETIME DEFAULT (datetime('now', 'localtime'))
);"
)?;
conn.pragma_update(None, "user_version", 2)?;
}
Ok(())
}
迁移规则
| 规则 | 说明 |
|---|---|
PRAGMA user_version |
当前 Schema 版本号 |
if version < N |
递增式迁移 |
| 永久保留 | 不删除旧版本代码 |
| 幂等性 | 使用 IF NOT EXISTS |
CRUD 操作模式
查询(Read)
/// 获取所有配置
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 ORDER BY key")?;
let configs = stmt
.query_map([], |row| {
Ok(AppConfig {
key: row.get(0)?,
value: row.get(1)?,
})
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(configs)
}
单条查询
/// 获取单个配置(返回 Option)
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")?;
let result = stmt
.query_row([key], |row| row.get::<_, String>(0))
.ok(); // 转换为 Option
Ok(result)
}
Upsert(Insert or Update)
/// 设置配置(如存在则更新)
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(())
}
删除(Delete)
/// 删除配置(返回是否删除成功)
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("DELETE FROM app_config WHERE key = ?1", [key])?;
Ok(affected > 0)
}
调用链示例(三层架构)
1. Database 层(数据访问)
// database/mod.rs
impl Database {
pub fn get_all_config(&self) -> Result<Vec<AppConfig>, AppError> {
// SQL 查询...
}
}
2. Service 层(业务逻辑)
// services/config_service.rs
impl ConfigService {
pub fn get_all(&self, db: &Database) -> Result<Vec<AppConfig>, AppError> {
// 可以在这里添加业务逻辑(如缓存、验证)
db.get_all_config()
}
}
3. Command 层(IPC 接口)
// commands/config.rs
use tauri::State;
#[tauri::command]
pub fn get_all_config(db: State<'_, Database>) -> Result<Vec<AppConfig>, String> {
db.get_all_config()
.map_err(|e| e.to_string())
}
4. 前端调用
// src/lib/api/index.ts
import { invoke } from "@tauri-apps/api/core";
export const configApi = {
getAll: () => invoke<AppConfig[]>("get_all_config"),
set: (key: string, value: string) =>
invoke<void>("set_config", { key, value }),
};
// 组件中使用
const configs = await configApi.getAll();
数据库设计规范
建表模板
CREATE TABLE IF NOT EXISTS {table_name} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- 业务字段
name TEXT NOT NULL,
status INTEGER DEFAULT 1, -- 0: 禁用, 1: 正常
-- 审计字段
created_at DATETIME DEFAULT (datetime('now', 'localtime')),
updated_at DATETIME DEFAULT (datetime('now', 'localtime'))
);
SQLite 类型映射
| SQLite 类型 | Rust 类型 | TypeScript 类型 |
|---|---|---|
| INTEGER | i32 / i64 |
number |
| TEXT | String |
string |
| REAL | f64 |
number |
| BOOLEAN | bool (存为 0/1) |
boolean |
| DATETIME | String |
string |
| BLOB | Vec<u8> |
Uint8Array |
线程安全与错误处理
Mutex 安全加锁
// ✅ 正确:map_err 转换错误
let conn = self.conn.lock()
.map_err(|e| AppError::Custom(e.to_string()))?;
// ❌ 错误:unwrap 会 panic
let conn = self.conn.lock().unwrap(); // 永远不要这样做
SQL 注入防护
// ✅ 正确:使用 ? 占位符
conn.execute("SELECT * FROM users WHERE id = ?1", [id])?;
// ❌ 错误:字符串拼接(SQL 注入风险)
let sql = format!("SELECT * FROM users WHERE id = {}", id);
conn.execute(&sql, [])?;
常见错误
| 错误做法 | 正确做法 |
|---|---|
| 在前端直接操作数据库 | 所有数据库操作在 Rust 后端 |
使用 unwrap() 处理 Mutex |
使用 map_err 转换错误 |
| 字符串拼接 SQL | 始终使用 ? 占位符防注入 |
| 不做数据库迁移 | 使用 PRAGMA user_version 管理版本 |
| 数据库文件用绝对路径 | 使用 app_data_dir() 获取路径 |
| 忘记 WAL 模式 | pragma_update(None, "journal_mode", "WAL") |