🧬

Dioxus で SurrealDB を組み込みモードで実行してみる

2024/04/08に公開

概要

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 のセットアップ

  1. SDK をインストールします。Architecture は Single-node (SpeeDB) を選択します。

    cargo add surrealdb
    cargo add surrealdb --features kv-speedb
    
  2. 依存関係をインストールします。自分の環境では clang のインストールが必要でした。

    cargo add serde --features derive
    cargo add tokio --features macros,rt-multi-thread
    cargo add futures
    sudo apt install clang
    

画面の実装

  1. 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!" }
                    }
                }
            }
        }
    }
    
  2. 画面デザインを実装します。

    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;
    }
    
  3. プロジェクトを実行すると次の画面が表示されます。

ロジックの実装

  1. DB の構造体を実装します。

    src/main.rs
    use 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
        }
    }
    
  2. App コンポーネントに DB から取得したデータを保持する data_state を実装します。

    src/main.rs
        let mut data_state = use_signal(|| Vec::<Person>::new());
    
  3. App コンポーネントに DB と data_state を同期するための Coroutine を実装します。

    src/main.rs
        use 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);
                    }
                }
            }
        });
    
  4. Submit ボタンを押下したらフォームの入力値を DB に保存して、data_state の値を表示するように App コンポーネントを修正します。

    src/main.rs
        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();
                    // 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}" }
                        }
                    }
                }
            }
        }
    
  5. プロジェクトを実行すると次の画面が表示されます。フォームに値を入力して 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