🧬
Dioxus で SurrealDB を組み込みモードで実行してみる
概要
Dioxus のデスクトップアプリで SurrealDB を組み込みモードで実行してみます。
参考情報
環境
- rustc 1.76.0 (07dca489a 2024-02-04)
- cargo 1.76.0 (c84b36747 2024-01-18)
- Dioxus v0.5.0
- dioxus-cli v0.5.0
- Visual Studio Code 1.87.2
- Dioxus VSCode Extension v0.5.0
準備
こちら を参考に Dioxus のセットアップ & 新規プロジェクトの作成を行います。
SurrealDB のセットアップ
-
SDK をインストールします。Architecture は Single-node (SpeeDB) を選択します。
cargo add surrealdb cargo add surrealdb --features kv-speedb
-
依存関係をインストールします。自分の環境では clang のインストールが必要でした。
cargo add serde --features derive cargo add tokio --features macros,rt-multi-thread cargo add futures sudo apt install clang
画面の実装
-
App コンポーネントを修正し、まずは固定値を表示するだけの画面を実装します。
src/main.rs#[component] fn App() -> Element { rsx! { link { rel: "stylesheet", href: "main.css" } h2 { "Registration Form" } form { id: "dataForm", label { r#for: "title", "Title:" } input { r#type: "text", id: "title", name: "title", required: true } label { r#for: "name", "First name:" } input { r#type: "text", id: "first-name", name: "first-name", required: true } label { r#for: "name", "Last name:" } input { r#type: "text", id: "last-name", name: "last-name", required: true } button { r#type: "submit", "Submit" } } h2 { "Data List" } table { id: "dataList", thead { tr { th { "Title" } th { "First name" } th { "Last name" } } } tbody { tr { td { "Static Title!" } td { "Static First name!" } td { "Static Last name!" } } } } } }
-
画面デザインを実装します。
assets/main.cssbody { font-family: Arial, sans-serif; margin: 0; padding: 0; } form { margin-bottom: 20px; } table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #dddddd; text-align: left; padding: 8px; } th { background-color: #f2f2f2; }
-
プロジェクトを実行すると次の画面が表示されます。
ロジックの実装
-
DB の構造体を実装します。
src/main.rsuse serde::{Deserialize, Serialize}; use surrealdb::engine::local::SpeeDb; use surrealdb::sql::Thing; use surrealdb::Surreal; struct Db { db: Surreal<surrealdb::engine::local::Db>, } #[derive(Debug, Serialize, Deserialize)] struct Name { first: String, last: String, } #[derive(Debug, Serialize, Deserialize)] struct Person { title: String, name: Name, } #[derive(Debug, Deserialize)] struct Record { #[allow(dead_code)] id: Thing, } impl Db { async fn get_person_all(&self) -> Result<Vec<Person>, surrealdb::Error> { self.db.select("Person").await } async fn set_person(&self, person: Person) -> Result<Vec<Record>, surrealdb::Error> { self.db.create("Person").content(person).await } }
-
App コンポーネントに DB から取得したデータを保持する data_state を実装します。
src/main.rslet mut data_state = use_signal(|| Vec::<Person>::new());
-
App コンポーネントに DB と data_state を同期するための Coroutine を実装します。
src/main.rsuse futures::stream::StreamExt; // App コンポーネント内に実装します。 let sync_data_task = use_coroutine(|mut rx: UnboundedReceiver<Person>| { async move { // Create database connection let db_result = Surreal::new::<SpeeDb>("data").await; // DB のデータを保存するフォルダに `data` を指定します。 match db_result { Ok(db) => { // Select a specific namespace / database let _ = db.use_ns("namespace").use_db("database").await; let db = Db { db, }; // 初期表示用にデータを DB から取得して data_state に設定します。 let people_result = db.get_person_all().await; match people_result { Ok(people) => { // dbg!(people); data_state.set(people); } Err(err) => { log::error!("Failed to get people records: {:?}", err); } } // Coroutine に値が送信されたら実行する処理です。 while let Some(person) = rx.next().await { // 送信された値を DB に保存します。 let created = db.set_person(person).await; let _ = dbg!(created); // DB に保存したデータをデバッグ用に出力します。 // 保存後にデータを再取得して data_state に設定します。 let people_result = db.get_person_all().await; match people_result { Ok(people) => { // dbg!(people); data_state.set(people); } Err(err) => { log::error!("Failed to get people records: {:?}", err); } } } } Err(err) => { log::error!("Failed to create database connection: {:?}", err); } } } });
-
Submit ボタンを押下したらフォームの入力値を DB に保存して、data_state の値を表示するように App コンポーネントを修正します。
src/main.rsrsx! { link { rel: "stylesheet", href: "main.css" } h2 { "Registration Form" } form { id: "dataForm", onsubmit: move |ev| { let title = ev.values().get("title").unwrap().as_value(); let first_name = ev.values().get("first-name").unwrap().as_value(); let last_name = ev.values().get("last-name").unwrap().as_value(); // Submit ボタンを押下したらフォームの入力値を Coroutine に送信します。 sync_data_task .send(Person { title: title, name: Name { first: first_name, last: last_name, }, }); }, label { r#for: "title", "Title:" } input { r#type: "text", id: "title", name: "title", required: true } label { r#for: "name", "First name:" } input { r#type: "text", id: "first-name", name: "first-name", required: true } label { r#for: "name", "Last name:" } input { r#type: "text", id: "last-name", name: "last-name", required: true } button { r#type: "submit", "Submit" } } h2 { "Data List" } table { id: "dataList", thead { tr { th { "Title" } th { "First name" } th { "Last name" } } } tbody { // data_state の値を表示します。 for data in data_state.iter() { tr { td { "{&data.title}" } td { "{&data.name.first}" } td { "{&data.name.last}" } } } } } }
-
プロジェクトを実行すると次の画面が表示されます。フォームに値を入力して Submit ボタンを押下すると DB に登録してデータ一覧に表示されます。
Full Code
Cargo.toml
[package]
name = "surrealdb"
version = "0.1.0"
authors = ["st-little"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus = { version = "0.5", features = ["desktop"] }
# Debug
log = "0.4.19"
dioxus-logger = "0.4.1"
surrealdb = { version = "1.3.1", features = ["kv-speedb"] }
serde = { version = "1.0.197", features = ["derive"] }
tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] }
futures = "0.3"
assets/main.css
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
form {
margin-bottom: 20px;
}
table {
border-collapse: collapse;
width: 100%;
}
th,
td {
border: 1px solid #dddddd;
text-align: left;
padding: 8px;
}
th {
background-color: #f2f2f2;
}
src/main.rs
#![allow(non_snake_case)]
use dioxus::prelude::*;
use log::LevelFilter;
use serde::{Deserialize, Serialize};
use surrealdb::engine::local::SpeeDb;
use surrealdb::sql::Thing;
use surrealdb::Surreal;
use futures::stream::StreamExt;
struct Db {
db: Surreal<surrealdb::engine::local::Db>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Name {
first: String,
last: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct Person {
title: String,
name: Name,
}
#[derive(Debug, Deserialize)]
struct Record {
#[allow(dead_code)]
id: Thing,
}
impl Db {
async fn get_person_all(&self) -> Result<Vec<Person>, surrealdb::Error> {
self.db.select("Person").await
}
async fn set_person(&self, person: Person) -> Result<Vec<Record>, surrealdb::Error> {
self.db.create("Person").content(person).await
}
}
fn main() {
// Init debug
dioxus_logger::init(LevelFilter::Info).expect("failed to init logger");
dioxus::launch(App);
}
#[component]
fn App() -> Element {
let mut data_state = use_signal(|| Vec::<Person>::new());
let sync_data_task = use_coroutine(|mut rx: UnboundedReceiver<Person>| {
async move {
// Create database connection
let db_result = Surreal::new::<SpeeDb>("data").await; // データを保存するフォルダに `data` を指定します。
match db_result {
Ok(db) => {
// Select a specific namespace / database
let _ = db.use_ns("namespace").use_db("database").await;
let db = Db {
db,
};
// 初期表示用にデータを DB から取得して data_state に設定します。
let people_result = db.get_person_all().await;
match people_result {
Ok(people) => {
// dbg!(people);
data_state.set(people);
}
Err(err) => {
log::error!("Failed to get people records: {:?}", err);
}
}
// Coroutine に値が送信されたら実行する処理です。
while let Some(person) = rx.next().await {
// 送信された値を DB に保存します。
let created = db.set_person(person).await;
let _ = dbg!(created); // DB に保存したデータをデバッグ用に出力します。
// 保存後にデータを再取得して data_state に設定します。
let people_result = db.get_person_all().await;
match people_result {
Ok(people) => {
// dbg!(people);
data_state.set(people);
}
Err(err) => {
log::error!("Failed to get people records: {:?}", err);
}
}
}
}
Err(err) => {
log::error!("Failed to create database connection: {:?}", err);
}
}
}
});
rsx! {
link { rel: "stylesheet", href: "main.css" }
h2 { "Registration Form" }
form {
id: "dataForm",
onsubmit: move |ev| {
let title = ev.values().get("title").unwrap().as_value();
let first_name = ev.values().get("first-name").unwrap().as_value();
let last_name = ev.values().get("last-name").unwrap().as_value();
sync_data_task
.send(Person {
title: title,
name: Name {
first: first_name,
last: last_name,
},
});
},
label { r#for: "title", "Title:" }
input { r#type: "text", id: "title", name: "title", required: true }
label { r#for: "name", "First name:" }
input {
r#type: "text",
id: "first-name",
name: "first-name",
required: true
}
label { r#for: "name", "Last name:" }
input {
r#type: "text",
id: "last-name",
name: "last-name",
required: true
}
button { r#type: "submit", "Submit" }
}
h2 { "Data List" }
table { id: "dataList",
thead {
tr {
th { "Title" }
th { "First name" }
th { "Last name" }
}
}
tbody {
for data in data_state.iter() {
tr {
td { "{&data.title}" }
td { "{&data.name.first}" }
td { "{&data.name.last}" }
}
}
}
}
}
}
Discussion