Rust勉強にTauriやsqlxやSvelteも盛ってみる。
作業記録・備忘録なのでちゃんとした情報求めていたら他を探した方が有意義と思う。
データ移行だるいから助けてほしいので支援ツールを作る。
- データ構造を2つ以上登録
- 移行プロジェクトを作成
- 移行元データ構造と移行先データ構造を指定
- 移行元から移行先へ列の関係性を指定
- 移行プロジェクトからデータ出力
- SQL
- CSV
そんなに知識ないけど Rust/Tauri/Sqlx/Svelte/TypeScript と欲張っておく。
入れておくもの
# cargo tauri init 等 carg tauri * なコマンド
cargo install tauri-cli
入れておくと良さげなもの。
# cargo パッケージ更新が楽になる cargo upgrade が使えたりする
cargo install cargo-edit
ここに沿うけどリポジトリ直下に展開する。
Tauri プロジェクトをサクッと作るならこれ。
Svelte テンプレートだと Rust ソース用に src-tauri
ができるのでほとんど手を加えなくて良さそう。
npm create tauri-app
rust-version = "1.57"
だった。rust-version = "1.62"
に書き換えて良いものかな。
アプリケーションで使う Rust バージョンだから多分良いと思うけど。
設計資料も vivliostyle をあえて使っていくぞ。
業務にも使いたいので設計資料が別出しできると良い(Office/PDF な会社なので)。
# ビルド
cargo tauri build
# 開発実行
cargo tauri dev
tauri.conf.json
の beforeBuildCommand
の値が npm run build
となっているか、 beforeDevCommand
の値が npm run dev
となっているか確認。
rust コードは src-tauri
配下で書く。分けてるリポジトリも有ったけどイマイチわからんかった。
参考になるのは awesome-tauri
これ使えば sqlx を使うのに必要な状態は作れるっぽい?
example に Todo アプリが有るのでそれを参考にする。
使うのはとりあえず sqlite3
Installation 見ながら、 Cargo.toml
に案内通り書いてみると tag 見つからんとかでエラー。
リポジトリ見ると「確かにー」なんだけど。下記にして cargo update
はうまくいったわよ
[dependencies.tauri-plugin-sql]
git = "https://github.com/tauri-apps/tauri-plugin-sql"
features = ["sqlite"]
branch = "release"
一旦 tauri-plugin-sql は外して様子見、Rust から使うのに必要なかったりして。。。
っぱ、Rust 側に DB は制御してほしいので、sqlx-cli 使って DB 構成する方向で。
下記でスキーマ定義のマイグレーション作っておく。
sqlx migrate add schema
下記で sqlite データベースを作成して、マイグレーション走らせる。
sqlx db create --database-url "sqlite:database.db"
sqlx migrate run --database-url "sqlite:database.db"
create_db なるものが下記の Issue に有った。
これに似た感じで実装したところ、下記を自動化できた。
sqlx db create --database-url "sqlite:database.db"
sqlx migrate run --database-url "sqlite:database.db"
で、下記が実装。わからないことだらけなので #[async_std::main]
のことは分かっちゃいない。一応、async_std
の features
にある attributes
に入ってることは分かった。Rust の非同期 Crate (?)多くない?
#[async_std::main]
async fn main() -> Result<(), sqlx::Error> {
create_db().await?;
tauri::Builder::default()
.run(tauri::generate_context!())
.expect("error while running tauri application");
Ok(())
}
// DB 作成
use sqlx::{migrate::MigrateDatabase, sqlite::SqlitePool};
async fn create_db() -> Result<SqlitePool, sqlx::Error> {
use std::path::Path;
let db_url: &str = "sqlite:database.db";
if !sqlx::Sqlite::database_exists(&db_url).await? {
sqlx::Sqlite::create_database(&db_url).await?;
}
let db: SqlitePool = SqlitePool::connect(&db_url).await?;
sqlx::migrate::Migrator::new(Path::new("./migrations"))
.await?
.run(&db)
.await?;
Ok(db)
}
async_std
は標準 API の非同期バージョンっていう Crate。
tokio
はネットワークアプリケーションの実装に必要な API を非同期に処理しようっていう Crate。
tokio
で十分な気もするけど async_std
だともっと細かい所も非同期処理に……ってできるのかな?
Svelte で SPA な時にハッシュベースルーティングを担ってくれる svelte-spa-router
を導入。SPA も初めてなので、ちょっとでも「いつもの感」を維持する為ルーティングしたかった。
ナビゲーションのクラス制御でちょっとてこずったのでメモ。
js 標準の location.href
取得して「今どこの配下?」を視覚的に表現しようとして、リアクティブステートメントで確認させて~ってやってみたけど上手くいかなかった。
何となく、SPA でルーティングしてるからかな~、なんて思って svelte-spa-router
の location
でイメージ通りのことが出来そうだったので試したところ上手くいった。
<script lang="ts">
import { location } from "svelte-spa-router";
let nowGroup: string = "top";
function getNowGroup(location: string) {
const reDataStructure: RegExp = new RegExp("data-structure");
const reMigrateProject: RegExp = new RegExp("migrate-project");
if (reDataStructure.test(location)) {
nowGroup = "data-structure";
} else if (reMigrateProject.test(location)) {
nowGroup = "migrate-project";
} else {
nowGroup = "top";
}
}
$: getNowGroup($location);
</script>
で、nowGroup によって class を変化させるってわけ。
共通で参照できるクラスを定義したものの JS に変換するとダメなの(プロパティの型指定を指される)、って言われちゃったので調べてみたところ、rollup.config.js で typescript のところに rootDir を指定しないといけないっぽい。
事前に js が生成されて解釈できるようになるっぽい。
他の環境で動かなかったとき確認すること
rustup update
npm install
- C++ ビルドツールの有無★
- Visual Studio Installer から C++ デスクトップ開発を追加するなど
open-color を npm でインストールした後でバンドルするのに手間取ったのでメモ
main.ts
に import "open-color/open-color.css";
を追加したらうまくいった。
フォーム変更の確認は SPA だとどうやってるんだろうか。
やっとこさバックエンドを進めてく。
「str
, String
って何の差があるの?」「Vec
ってなに?」と思ったので公式リファレンス読んでまとめ。
データ型
そもそもデータ型にはスカラ型、複合型がある。
スカラ型はプリミティブ型に当たる。
type kind | type name | type string | description |
---|---|---|---|
スカラ型 | 整数型 | i8 | 8bit 符号有り整数 |
スカラ型 | 整数型 | i16 | 16bit 符号有り整数 |
スカラ型 | 整数型 | i32 | 32bit 符号有り整数, 標準且つ最速 |
スカラ型 | 整数型 | i64 | 64bit 符号有り整数 |
スカラ型 | 整数型 | u8 | 8bit 符号無し整数 |
スカラ型 | 整数型 | u16 | 16bit 符号無し整数 |
スカラ型 | 整数型 | u32 | 32bit 符号無し整数 |
スカラ型 | 整数型 | u64 | 64bit 符号無し整数 |
スカラ型 | 浮動小数点数型 | f32 | 32bit 浮動小数点数 |
スカラ型 | 浮動小数点数型 | f64 | 64bit 浮動小数点数, 標準 |
スカラ型 | 論理値型 | bool | 論理値 |
スカラ型 | 文字型 | char | 16bit 符号無し整数 |
複合型 | タプル型 | (type, type, ..., type) | 異なる型の固定長のデータの集まり |
複合型 | 配列型 | [type; size] | 同じ型の固定長のデータの集まり |
整数型は接頭辞 0x
, 0o
, 0b
を付けられ、16 進数、8 進数、2 進数を見たまま書ける(整数リテラル)。
タプル型は let (x, y, z) = <tuple>
とすると分配される(<tuple>
はタプル型のデータ)。
配列型は let a = [3; 5]
と書くことで同値の入った配列を作れる。
Vec
, str
, String
とか出てこなかったな。
ベクタ型
Vec<T>
で表現する特定型のコレクション型。まあリスト。
文字列型
文字列スライス型: str
文字列型: String
スライス型は所有権が無くコレクションの一連の要素を参照。
※&
は C 言語さんと同じ参照を示す。Rust では所有権をもらわずに値参照できるよ。
とりあえず違いは分かったのでここまで。
~.await?
の ?
はパニック発生時にエラーを挙げてくれる。raise
とか throw
とかのような。
エラー発生の場合は即時エラー値を返す、エラー発生しなかったら呼び出した関数の返り値を返す。
Result<T, E>
や Option<T>
から T
のデータを取り出してくれる unwrap()
。
T
のデータを取り出せなかったときに panic!
起こしてエラー発生。
unwrap()
の代わりに expect()
で任意メッセージで panic!
してもらえる。
unwrap_or()
だと panic!
った時に任意値を返せる。
sqlx によるクエリ作成するときのメソッドメモ。メソッドでなくて関数の方が良いかしら?
-
sqlx::query<DB>()
: 単純にクエリ作成 -
sqlx::query_as<DB, O>()
:O
はsqlx_core::from_row::FromRow
(データベースから返された行から作成できるレコード)を派生した(derive
)構造体に基づいてデータベースからの返り値をマッピングする前提でクエリ作成。
多分よくあるメソッドの使い分けで良いと思う。
文 | どっち | 実行 |
---|---|---|
SELECT |
sqlx::query_as<DB, O>() |
fetch(executer) , fetch_all(executer) , fetch_one(executer) ,fetch_optional(executer)
|
INSERT , UPDATE , DELETE
|
sqlx::query<DB>() |
execute(executer) |
後者は PostgreSQL で RETURNING
せずに INSERT
文使ったりすると INSERT 0 1
が返ってくるだろうからエラーになると思う。SELECT
文でないときは使わなくていいと思う。
他言語の同様なモジュール・ライブラリにもそんな使い分けあったよなぁと思いつつ。
sqlx 特有かわからないけど INSERT
文を実行したら、結果として Ok(SqliteQueryResult { changes: 1, last_insert_rowid: 2 })
が返ってくる。追加したデータの PK が返ってくるようになってるっぽい?
Rust 側とフロントエンド側のやり取りにおける引数渡しについて、わかりづらかったのでメモ。
[tauri::command]
を指定した Rust 側の関数を tauri::app::Builder.invoke_handler()
でコマンドとして登録できるが、この引数渡し。
下記のように定義した Rust コマンドはその下の TypeScript/JavaScript で呼び出せるよ。Rust 側はスネークケース、TypeScript/JavaScript ではキャメルケースに。
#[tauri::command]
pub fn command(arg_any: i64) -> bool { ... }
fn main() -> Result<(), sqlx::Error> {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
command,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
import { invoke } from "@tauri-apps/api/tauri";
function anyFunction() {
invoke("command", { argAny: 1 });
}
引数に構造体を使うこともできる。構造体にシリアライズ・デシリアライズをしてくれるフレームワーク serde
の serde::Serialize
, serde::Deserialize
から派生させておく。
構造体の中身のパラメータ名はスネークケースのままで良かった。
#[serde::Serialize, serde::Deserialize]
struct command_data {
param_one: String,
param_two: i64
}
#[tauri::command]
pub fn command(arg_any: command_data) -> bool { ... }
fn main() -> Result<(), sqlx::Error> {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
command,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
import { invoke } from "@tauri-apps/api/tauri";
function anyFunction() {
let struct = {
param_one: "xxx",
param_two: 1
}
invoke("command", {
argAny: struct
});
}
Rust 側からの返り値が構造体の場合も中身にアクセスするのは Rust 側通り(スネークケース)
支援ツールの考え方変えちゃって多分更新しないから閉じる