Building a Tauri App with Sqlx+Sqlite+SvelteKit
Summary
了解如何使用 Axum 构建一个 Websocket 服务。
Let's get started
Add Dependencies
首先在Cargo.toml中增加如下依赖:
[dependencies]
tokio = { version = "1.33.0", features = ["time", "rt", "macros"] }
sqlx = { version = "0.7.2", features = ["sqlite", "runtime-tokio"] }
futures = "0.3"
Database Setup
我们首先创建一个函数,用于设置 sqlite
数据库,并创建一个连接池。
初始时,如果不存在数据库,需要创建一个新的数据库文件。
type Db = Pool<Sqlite>;
async fn setup(app: &App) -> Db {
let mut path = app
.path()
.app_data_dir()
.expect("could not get data_dir");
match std::fs::create_dir_all(path.clone()) {
Ok(_) => {}
Err(err) => {
panic!("error creating directory {}", err);
}
};
path.push("db.sqlite");
let result = OpenOptions::new().create_new(true).write(true).open(&path);
match result {
Ok(_) => println!("database file created"),
Err(err) => match err.kind() {
std::io::ErrorKind::AlreadyExists => println!("database file already exists"),
_ => {
panic!("error creating databse file {}", err);
}
},
}
let db = SqlitePoolOptions::new()
.connect(path.to_str().unwrap())
.await
.unwrap();
sqlx::migrate!("./migrations").run(&db).await.unwrap();
db
}
该初始化函数含有一个参数以引用 tauri::App
,我们可以通过该参数获得应用程序数据应存储的正确路径(兼容任何操作系统)。
接下来,我们将 db.sqlite
添加上述获取的路径上,得到数据库文件的完整路径。然后我们尝试创建数据库文件,如果创建了新文件,我们就会打印出来,如果文件已经存在,则略过。
最后,我们使用 sqlx
创建一个数据库链接池并返回。
Migrations using sqlx-cli
我们已经连接到我们的 sqlite
数据库,但我们还没有设置任何表,这就是 Migration
的用武之地。
首先我们需要安装 sqlx-cli
命令行工具:
cargo install sqlx-cli
首先我们要进入到 src-tauri
目录, 因为执行命令时, 它会根据项目的 Cargo.toml
文件的相对路径来定位文件:
cd src-tauri
sqlx migrate add create_todos_table
现在我们可以打开新生成的迁移文件,添加新的数据库表:
CREATE TABLE organization (
id INTEGER PRIMARY KEY,
slug TEXT,
name TEXT,
contact TEXT,
contact_telecom TEXT,
status VARCHAR(30)
);
现在,我们有了一个迁移文件,我们将添加一行代码,以便在每次启动应用时自动更新数据库结构。
我们将在 setup
函数中创建 sqlite
链接池后添加该语句:
sqlx::migrate!("./migrations").run(&db).await.unwrap();
Add Database Pool into global state
要在整个应用程序中使用 sqlite
链接池, 要使用 Tauri
提供的全局状态托管机制。
首先我们将创建一个表示全局应用状态的结构体:
struct AppState {
db: Db,
}
Tauri App Launch
因为 sqlx
是一个异步的数据处理库, 首先我们需要将 main
函数修改为可以支持异步,这需要引入 tokio
依赖库:
#[tokio::main]
async fn main() {
}
接下来,我们将在 main
函数中设置我们的数据库并全局管理它:
#[tokio::main]
async fn main() {
let app = tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
// 在没有添加Command之前,generate_handler![]宏内为空
.invoke_handler(tauri::generate_handler![])
.build(tauri::generate_context!())
.expect("error building the app");
let db = setup(&app).await;
app.manage(AppState {db});
app.run(|_, _| {});
}
Oraganization CRUD
新增机构信息:
#[tauri::command]
pub async fn create_organization(state: tauri::State<'_, AppState>, organization: CreateOrganization) -> Result<(), String> {
let db = &state.db;
println!("insert organization data");
sqlx::query("INSERT INTO organization (slug, name, contact, contact_telecom, status) VALUES (?1, ?2, ?3, ?4, ?5)")
.bind(organization.slug)
.bind(organization.name)
.bind(organization.contact)
.bind(organization.contact_telecom)
.bind(RecordStatus::Active)
.execute(db)
.await
.map_err(|e| format!("Error saving organization: {}", e))?;
Ok(())
}
获取所有的机构信息:
// add this to your use statements
use futures::TryStreamExt;
#[tauri::command]
pub async fn get_organizations(state: tauri::State<'_, AppState>) -> Result<Vec<Organization>, String> {
let db = &state.db;
let todos: Vec<Organization> = sqlx::query_as::<_, Organization>("SELECT * FROM organization")
.fetch(db)
// provide by futures crate
.try_collect()
.await
.map_err(|e| format!("Failed to get organizations {}", e))?;
Ok(todos)
}
更新机构信息:
#[tauri::command]
async fn update_organization(state: tauri::State<'_, AppState>, organization: Organization) -> Result<(), String> {
let db = &state.db;
sqlx::query("UPDATE organization SET slug = ?1, name = ?2, contact = ?3, contact_telecom = ?4, status = ?5 WHERE id = ?6")
.bind(organization.slug)
.bind(organization.name)
.bind(organization.contact)
.bind(organization.contact_telecom)
.bind(organization.status)
.bind(organization.id)
.execute(db)
.await
.map_err(|e| format!("could not update organization {}", e))?;
Ok(())
}
删除机构信息:
#[tauri::command]
async fn delete_organization(state: tauri::State<'_, AppState>, id: u16) -> Result<(), String> {
let db = &state.db;
sqlx::query("DELETE FROM organization WHERE id = ?1")
.bind(id)
.execute(db)
.await
.map_err(|e| format!("could not delete organization {}", e))?;
Ok(())
}
Call Organization CRUD in Sveltekit
在我们从 Sveltekit
调用 CRUD
方法之前,我们需要将这些方法添加到 main
函数的 tauri::generate_handler![]
宏中, 以使 Tauri
前端可调用。
tauri::generate_handler![create_organization, get_organizations, update_organization, delete_organization]
然后,我们可以打开相应的 +page.svlete
文件, 添加调用函数来添加进行机构信息的相应操作:
import { invoke } from '@tauri-apps/api/core';
let organization = {
slug: slug,
name: name,
contact: contact,
contact_telecom: telecom,
};
async function addOrganization(organization) {
return await invoke("create_organization", { organization });
}
async function listOrganizations() {
return await invoke("get_organizations");
}
async function updateOrganization(organization) {
return await invoke("update_organization", { organization });
}
async function deleteOrganization(id) {
return await invoke("delete_organization", { id });
}