tauri-plugin-sql v2を使っていてもRust command側のqueryはsqlxを使う
tauri-plugin-sql は、frontendがJavaScriptからdatabaseへアクセスしたい時に便利です。WebViewにDB APIを提供し、Rust側で一般的なSQL backendを支えます。
しかしTauriアプリにRust commandがあると、別の疑問が出ます。Rust commandは tauri-plugin-sql 経由でDBを呼ぶべきか、それともRust database crateを直接使うべきか。
実用的な答えは、frontend-side DB accessには tauri-plugin-sql、Rust command handler内では sqlx を直接使う、です。
判断基準
分け方です。
React / TypeScriptが簡単なDB accessを必要とする
-> tauri-plugin-sql JavaScript API
Rust commandがDB accessを必要とする
-> Rust内でsqlx
両方が必要
-> schemaとmigrationは共有し、DB callはそれぞれnativeにする
pluginはRust command用の汎用database layerではありません。主にfrontendへdatabase operationを公開するbridgeです。すでにRust内にいるなら、Rust database libraryを直接使うほうが明確でtestしやすいです。
Rust command内ではsqlxを使う
Rust commandはapp stateからpoolを受け取り、直接queryできます。
use tauri::State;
use sqlx::{Row, SqlitePool};
pub struct AppState {
pub db: SqlitePool,
}
#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Campaign {
pub id: i64,
pub name: String,
pub point_value: i64,
}
#[tauri::command]
pub async fn list_campaigns(
state: State<'_, AppState>,
) -> Result<Vec<Campaign>, String> {
let rows = sqlx::query(
"SELECT id, name, point_value FROM campaigns ORDER BY id DESC"
)
.fetch_all(&state.db)
.await
.map_err(|error| error.to_string())?;
Ok(rows
.into_iter()
.map(|row| Campaign {
id: row.get("id"),
name: row.get("name"),
point_value: row.get("point_value"),
})
.collect())
}
commandはtyped Rust structを返し、frontendは通常のTauri command responseを受け取ります。
databaseは1回初期化する
定期的にDBを使うなら、commandごとに再接続しないようにします。startupでpoolを作り、managed stateに入れます。
pub async fn create_db_pool(app_data_dir: &std::path::Path) -> Result<SqlitePool, sqlx::Error> {
let db_path = app_data_dir.join("app.sqlite");
let url = format!("sqlite:{}?mode=rwc", db_path.display());
SqlitePool::connect(&url).await
}
Tauri builderでmanageします。
tauri::Builder::default()
.manage(AppState { db })
.invoke_handler(tauri::generate_handler![list_campaigns])
.run(tauri::generate_context!())?;
DB lifecycleをUI component内に隠さず、所有者を明確にします。
Rustがschemaを持つならmigrationもRust側に寄せる
Rust commandがbusiness ruleやaggregationを持つなら、schema initializationもRust側に寄せます。
pub async fn run_migrations(pool: &SqlitePool) -> Result<(), sqlx::Error> {
sqlx::migrate!("./migrations").run(pool).await
}
schemaに依存するcommandが使われる前にmigrationを走らせます。
JavaScript plugin APIが向く場面
plugin APIが向く場面です。
- UIが軽いlocal preferenceやCRUDを扱う
- operationがpresentation寄り
- Rust-only parsingやfilesystem権限が不要
- 低リスク画面でRust commandを増やしたくない
Rust-side sqlx が向く場面です。
- queryがTauri commandの一部
- Rust-only libraryを使う
- filesystem、parsing、aggregation、batch performanceが必要
- DB behaviorをRust testで確認したい
- command responseをtyped Rust structにしたい
business ruleを両側に分散しない
危険なのは「plugin plus sqlx」ではありません。同じbusiness ruleを両側に重複実装することです。
たとえばcampaign status validationをある画面ではJavaScript、別画面ではRustで行うのは避けます。source of truthを1つにします。
検証チェック
- database file pathはapp data directoryから作る
- query command前にmigrationが走る
- Rust commandは必要に応じてsqlxを直接使う
- frontend plugin queryがRust business ruleを重複しない
- return structに明示的なserde namingがある
- migrationとcommand queryのtestがある