在桌面应用中,很多场景需要持久化数据——用户配置、本地缓存、离线数据仓库等。相比于简单的 JSON 文件或 localStorage,关系型数据库在处理复杂查询、数据一致性和并发读写时有不可替代的优势。Tauri 可以直接集成 SQLite,通过 Rust 后端操作数据库,再通过 IPC 把结果传给前端,整个过程高效、安全,且不会增加额外的运行时依赖。
为什么选择 SQLite?
- 零配置,零服务:SQLite 是嵌入式数据库,不需要单独安装或启动服务,数据库就是一个文件,随应用一起分发。
- 体积极小:SQLite 库本身只有几百 KB,编译进 Tauri 的 Rust 内核后基本不增加包体积。
- 完整 SQL 支持:支持大多数标准 SQL 语法、事务、索引、触发器等,甚至支持 JSON 查询。
- 跨平台一致:文件格式跨平台完全兼容,Windows 上的数据库文件可以直接拷贝到 macOS 或 Linux 使用。
- 并发处理:SQLite 支持多线程读,单线程写,在桌面应用场景下完全够用,且避免了客户端/服务器数据库的维护成本。
集成方式
Tauri 不强制特定数据库库,但社区最常用的方案是通过 Rust 的 rusqlite 库操作 SQLite。下面是一个最小化示例,展示如何初始化数据库、创建表、插入数据并把结果返回给前端。
1. 添加依赖
在 src-tauri/Cargo.toml 中加入:
[dependencies]
rusqlite = { version = "0.31", features = ["bundled"] }
其中 features = ["bundled"] 会将 SQLite 源码编译进应用,避免依赖系统库。
2. 初始化数据库连接
在应用启动时建立连接,通常放在 Tauri 的 setup 钩子中,并通过 Tauri 的状态管理让各命令共享连接。
use tauri::Manager;
use rusqlite::Connection;
use std::sync::Mutex;
struct DbState(Mutex<Connection>);
fn main() {
tauri::Builder::default()
.setup(|app| {
let conn = Connection::open(app.path().app_data_dir()?.join("mydb.sqlite"))?;
conn.execute(
"CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)",
[],
)?;
app.manage(DbState(Mutex::new(conn)));
Ok(())
})
.invoke_handler(tauri::generate_handler![add_note, get_notes])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
3. 定义命令
前端通过 Tauri 命令调用这些函数:
#[tauri::command]
fn add_note(state: tauri::State<DbState>, title: String, content: String) -> Result<(), String> {
let conn = state.0.lock().map_err(|e| e.to_string())?;
conn.execute("INSERT INTO notes (title, content) VALUES (?1, ?2)", &[&title, &content])
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
fn get_notes(state: tauri::State<DbState>) -> Result<Vec<Note>, String> {
let conn = state.0.lock().map_err(|e| e.to_string())?;
let mut stmt = conn.prepare("SELECT id, title, content, created_at FROM notes ORDER BY created_at DESC")
.map_err(|e| e.to_string())?;
let notes = stmt.query_map([], |row| {
Ok(Note {
id: row.get(0)?,
title: row.get(1)?,
content: row.get(2)?,
created_at: row.get(3)?,
})
}).map_err(|e| e.to_string())?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?;
Ok(notes)
}
#[derive(serde::Serialize)]
struct Note {
id: i32,
title: String,
content: String,
created_at: String,
}
4. 前端调用
import { invoke } from '@tauri-apps/api/tauri';
async function saveNote() {
await invoke('add_note', { title: '新笔记', content: '内容...' });
const notes = await invoke('get_notes');
console.log(notes);
}
注意事项
- 并发安全:
rusqlite::Connection不是Send的,所以需要用Mutex包裹。对于高频读写的场景,可以使用连接池(如r2d2-sqlite)或启用 WAL 模式提高并发。 - 数据库文件位置:推荐使用
app.path().app_data_dir()获取平台对应的数据目录,确保数据库文件不会因应用更新被覆盖或权限错误。 - 迁移管理:项目变大后建议引入轻量的 schema 迁移工具(如
refinery或手动维护版本表),避免直接改表导致用户数据丢失。 - 安全性:所有数据库操作都在 Rust 端执行,前端永远无法直接访问数据库文件,这比在 WebView 中操作 IndexedDB 或
localStorage更安全,也有利于防止 SQL 注入(参数化查询已用?1, ?2处理)。
实际应用场景
- 记事本 / 笔记应用:存储用户笔记、标签、附件路径。
- 本地缓存:将服务端数据镜像到本地,减少网络请求并支持离线查看。
- 日志分析工具:导入日志文件后存入 SQLite,利用 SQL 快速查询和聚合。
- 个人财务软件:管理收支记录、分类统计,支持复杂报表。
- 桌面端小游戏:存储分数、关卡进度等。
相比直接用 JSON 文件,SQLite 能让你在处理数千条数据时依然保持查询速度和数据完整性,而不会像文本文件那样出现读写冲突或性能急剧下降。
一句话总结:Tauri + SQLite 的组合把“桌面应用本地数据库”这件事做到了极致——零配置、高性能、跨平台一致,你只需专注业务逻辑,剩下的交给 Rust 和 SQLite 处理。