pagetakaBlog

最近リフォームと鳥取県日南町の記事多め。写真、PC、ネット等の話題も

Rust失敗録:「構造体」(struct)書いたが、sqlx用にderiveする必要が...

【Rust:sqlx+構造体でMariaDB読み取り】
【Rust:sqlx+構造体でMariaDB読み取り】

前説:非同期+sqlx+query_asでRDB読み取りタプルへ

爺、Rust触りだしたのは2月中旬からです。Rust+RDBは、同期読み取りから始め、tokioで非同期読み取り、変数の一種「タプル」に収め、ターミナルに値表示(println!)できました。これが前回までの状況です。

RustによるRDB読み取りでは、他言語のSQL経験が役にたちました。一方、型の扱いには翻弄されました。

  • 整数の処理はコレまでの経験(Python)でほぼできる
  • Rustでは、varchr、charなど文字型は、「String」として扱う
  • Rustでは、日付型、十進数型(Decimal)は、外部クレートが必要でsqlxへ追記必須

Rust的なトコロで混乱したです...。

「構造体+query_as! がRDB関係の鉄板でしょ!」と見通しておられた方からすると、ずいぶん「亀」な進行です。今回、「構造体」が登場します。

  • 構造体とコードの関係図を記事先頭近くに掲載したのでご参照ください。
  • 引き続き、前回までのコード(main.rs、Cargo.toml)を使い、少し修正します。
  • 今回は、外部クレート追加はありません。含める「型」により外部クレートが必要になります(例:NaiveDate → chrono、Decimal → rust_decimal など)。
  • 参考コードは、今回も「一行読み取り」です。チョットした爺の都合で「十進数型」を今回は使いません。次回お勉強予定ですので悪しからず。

構造体は、特製「仕切り付き弁当容器」の仕様

RDBから読み取るデータを、型も読み取れる「構造体」に変更したのが今回のポイントです(これまでは「タプル」)。

一行読取りの場合、

  1. structで明示する「構造体」は、特製「仕切り付き弁当容器」仕様(盛り付け済みの弁当ではない)
  2. SQLx(=sqlx)と構造体の関係確立は、derive 内の(sqlx::FromRow)が行う
    • FromRowが構造体への変換ルール(盛り付け手順書)
    • deriveは、その変換ルールを自動生成するマクロ
  3. SQLxは、deriveに従い、RDBから取得したデータを各SQL型に合わせRustの型へ自動変換(食材調理)
  4. 自動変換したデータは、SQL文直前にある「<_, 構造体名>」の仕様に従い、変数(今回はrow)に保管(盛り付け作業)
  5. 利用(今回はprintln!)は、「前項変数名.構造体項目名」を使い適宜書くと表示できた(駅弁店頭に並ぶ)、となるはず。やっと、今回のゴール到着

...手順長いですね。
最後の部分、例えば「row.date」 なのはメモしとかないと、爺すぐ忘れそう。

「構造体はSQL専用ではありません。derive内へ適応する記述を、CSV、JSON、メモリ上のデータなどごとに設定すれば、sql以外でも使えます」との旨をCopilotに教えてもらいました。

main.rsの #[derive(sqlx::FromRow, Debug)]とstruct

前回使用した十進数型、お勉強の都合で今回は使いません。

use (省略)

#[derive(sqlx::FromRow, Debug)]
struct StockData {
    num: i32,
    cd: String,
    date: NaiveDate,
}
#[tokio::main]
async fn main() -> (略)
  • #[ derive(sqlx::FromRow, Debug)]:SQL列名と構造体フィールド名を対応させる出発点
  • sqlxがSQLの型をRustの型に変換する中心
  • struct StockData {...}:構造体の名前と内容

単にstruct書いただけで「構造体」は働かず、爺はココで見事にコケたです。「derive」と内容記載で、SQLとstructの関係づけが明示され、sqlxがSQL行を構造体へ自動マッピングする準備ができるようです。用意したワンプレートに料理配置の「指示書」(= FromRow のお作法)が無かった...と。

構造体名の直前に置く「_」は”型推論の空欄"

「<_, 構造体名>」と書いとけば、Rustがうまく処理してくれる...。
Copilotによると、
<_, 構造体名> の「_」は型を書く場所で、Rustに型推論を任せ、具体的に書くのを省いている「_」
だそうです。

「構造体」使って、MariaDB読み取った main.rs コード

MariaDB に接続し、

  • query_as で 1 行だけ構造体に読み込む
  • match で Some/None を分岐して表示

というこれまでの記事の流れと同じです。

コードは次の通りです。

use sqlx::mysql::MySqlPoolOptions;
use chrono::{NaiveDate};
// use rust_decimal::Decimal;
#[derive(sqlx::FromRow, Debug)]
struct StockData {
    num: i32,
    cd: String,
    date: NaiveDate,
    // dec: Decimal, //今回未使用
}

#[tokio::main]
async fn main() ->std::result::Result<(), Box<dyn std::error::Error>>{
    connect_with_jpx()
    .await?;
    Ok(())
}

async fn connect_with_jpx() ->std::result::Result<(), Box<dyn std::error::Error>>{
    println!("RDB 接続します");

    let url ="mysql://user_name:password@192.168.x.x:3306/db_name";
    
    let pool = MySqlPoolOptions::new()
        .max_connections(5)
        .connect(url)
        .await?;
    let row =sqlx::query_as::<_, StockData> (
        "SELECT num, cd, date FROM table_name LIMIT 1"
        )
        .fetch_optional(&pool)
        .await?;
     
    match row {
        Some(row) => println!("num={}, cd={}, date={}", row.num, row.cd, row.date),
        None => println!("データがありません"),
    }
    Ok(())
}

補足説明:Cargo.toml、日付型、十進数型、そしてquery_as!

  • 今回は Cargo.toml 追加などありません
  • 日付型は、時刻を含まない単に「日付」です。
  • 十進数型(NULL含む)を扱うにはOption設定が必要です。次のお勉強にまわしました。
  • 鉄板のはず!「struct+query_as!」まであと少しです。

改めて「構造体はSQL専用ではありません」を追記するようCopilotに勧められました。