Daxia Blog
Uncategorized | Rust | WebUI | FHIR | Javascript | KB

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 });
}

About Daxia
我是一名独立开发者,国家工信部认证高级系统架构设计师,在健康信息化领域与许多组织合作。具备大型卫生信息化平台产品架构、设计和开发的能力,从事软件研发、服务咨询、解决方案、行业标准编著相关工作。
我对健康信息化非常感兴趣,尤其是与HL7和FHIR标准的健康互操作性。我是HL7中国委员会成员,从事FHIR培训讲师和FHIR测评现场指导。
我还是FHIR Chi的作者,这是一款用于FHIR测评的工具。