数据库:SQL 插件与 SQLite 实战

本地存储是桌面应用的刚需:用户数据、缓存、历史记录总要落盘。Tauri 官方提供 tauri-plugin-sql,用 Rust 的 sqlx 驱动 SQLite 与 MySQL,前端可以直接执行 SQL。本篇讲插件接入、连接管理、迁移与分层架构的最佳实践。

一、插件安装与初始化

先在 Rust 端添加插件依赖,并在 Builder 注册:

fn main() {
    tauri::Builder::default()
        .plugin(tauri_plugin_sql::Builder::default().build())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

前端安装对应 npm 包:

npm install @tauri-apps/plugin-sql

前端即可直接打开连接并执行 SQL,连接由 Rust 端持久持有:

import Database from '@tauri-apps/plugin-sql'

const db = await Database.load('sqlite:app.db')

await db.execute(
  'CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, title TEXT, updated INTEGER)'
)

await db.execute('INSERT INTO notes (title, updated) VALUES ($1, $2)', ['第一条', Date.now()])

const rows = await db.select('SELECT * FROM notes ORDER BY updated DESC')

注意 SQL 语句的写法以插件文档为准,不同后端的占位符风格可能不同。表结构变更建议走迁移机制,具体见第三节。

二、架构选择:前端直连还是 Rust 中转

tauri-plugin-sql 允许前端直接执行 SQL,但这不代表应该把所有 SQL 散在组件里。两种架构对比:

架构 说明 适用
前端直连 组件里写 SQL 原型、个人小工具
Rust 中转 SQL 集中在 Rust 命令,前端只调命令 中大型应用

生产建议用 Rust 中转:类型安全、可加业务校验、SQL 不暴露给前端。Rust 端直接用 sqlx 与连接池:

use sqlx::sqlite::SqlitePool;

pub struct AppState {
    pub pool: SqlitePool,
}

#[tauri::command]
async fn list_notes(state: tauri::State<'_, AppState>) -> Result<Vec<Note>, String> {
    sqlx::query_as::<_, Note>("SELECT id, title FROM notes")
        .fetch_all(&state.pool)
        .await
        .map_err(|e| e.to_string())
}

连接池放 State,随应用生命周期管理,写法见「全局状态:tauri::State 与共享数据」。

三、迁移管理

表结构总会演进。Rust 端用 sqlx 的 migrate 宏管理迁移:

async fn run_migrations(pool: &SqlitePool) -> Result<(), String> {
    sqlx::migrate!("./migrations")
        .run(pool)
        .await
        .map_err(|e| e.to_string())
}

迁移文件按版本编号,形如 0001_init.sql、0002_add_tags.sql。也可以在插件配置中声明,自动执行 migration,具体以插件文档为准。

迁移实践 说明
只增不改 历史迁移文件不可修改
逐步演进 每次变更一个文件
备份优先 升级前提醒用户导出数据
测试覆盖 新老版本都要跑通

四、插件配置与数据库位置

数据库文件放在应用数据目录,路径通过 app.path() 取得:

use tauri::Manager;

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            let app_data = app.path().app_data_dir().unwrap();
            println!("数据目录:{}", app_data.display());
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

访问数据目录对应的路径需要在 Capability 中配置 fs 作用域,权限细节见「权限模型:Capability 与作用域」。打包发布后,数据目录也是自动更新时必须保留的位置,跨版本兼容见「自动更新与版本分发」。

五、性能与维护

SQLite 单文件数据库足够支撑绝大多数桌面应用,但要注意几条:批量写入用事务,比逐条执行快一个量级;大结果集分页查询,别一次全取;定期 VACUUM 优化文件体积;索引按查询条件建。

场景 优化建议
批量插入 显式事务包裹
列表展示 LIMIT 分页
模糊搜索 LIKE 前缀或 FTS
导入导出 流式处理避免 OOM

数据库相关的 Rust 异步与并发知识见「异步命令与后台任务」与「所有权与线程安全:Send、Sync 与 Mutex」。写 SQL 与迁移文件时,用 Claude Code 指南 的 AI 辅助方式能显著提效;分析慢查询日志时 正则 mastery 是利器;Rust 结构体与 derive 的基础见 Rust 入门。

六、数据模型设计实战

SQLite 的灵活性容易让人跳过设计直接建表,业务一复杂就返工。几条经验:

原则 说明
唯一约束先行 该唯一的字段立刻加 UNIQUE
外键显式声明 开启 PRAGMA foreign_keys
时间用 INTEGER 存时间戳,展示时格式化
大字段单独表 正文与元数据分表
预留扩展字段 常用 JSON 列放动态字段

SQLite 默认不开启外键约束 SQLite 默认不开启外键约束,需要每次连接执行 PRAGMA foreign_keys = ON,否则级联删除不生效,这是最常被忽略的细节。

另一个高频问题是并发写:SQLite 有写锁,多个后台任务同时写会偶发锁冲突。解法是所有写操作走同一个连接或连接池的单写模式,配合 WAL 模式提升并发性能,具体参数以 SQLite 官方文档为准。

SQLite 在 Tauri 应用里几乎是默认选择:零服务器、单文件、事务可靠,配合 sqlx 的编译期 SQL 校验更是如虎添翼。数据库相关的具体 API 以插件与 sqlx 文档为准,遇到迁移问题优先回到官方示例核对。

最后一点经验:数据库迁移文件与业务代码同等重要,同样要 review、同样要测试。很多团队只看应用代码不看迁移,直到某次升级把用户数据加锁卡死。把迁移当作一次正式的功能变更对待,数据库就能安静地支撑应用跑很久。

数据库部分收束成四句话:选 SQLite、走 Rust 中转、迁移只增不改、写操作走事务。记住这四条就能稳稳支撑大部分桌面应用。性能调优等数据量真起来再做也不迟,过早优化在数据库上同样成立。

延伸阅读

  • 全局状态:tauri::State 与共享数据——连接池的标准存放位置
  • 异步命令与后台任务——异步查询的写法
  • Rust 入门:结构体与错误处理前置知识