刹那(せつな)の瞬き

Willkömmen! Ich heiße Setsuna. Haben Sie etwas Zeit für mich?

Rustでasync/awaitに対応したTiberiusからSQLServerに接続する

一年くらい前に Rust から ODBC ドライバで接続する方法を試してました。

その当時、直接 TDS プロトコルを扱う tiberius クレートも試してみたのですが、依存するクレートのバージョンを固定する必要があり、コードが書き辛い状態でした。

しかし、改めて確認したところ、いつの間にやら async/await に対応した tiberius クレートの開発が進んでます。

tokio だけでなく async-std にも対応しているとの事なので、今回は async-std を利用して CRUD なコードを試してみました。

1. 環境

2. プロジェクト

(1) 準備

適当なディレクトリにプロジェクトを作成します。(今回は ~/work/crud_mssql)

$ cd ~/work
$ cargo new crud_mssql
$ cd crud_mssql
(2) データベース

テスト用のデータベースを用意します。(今回は my_test_db)
※既存のデータベースを利用する場合は不要です。

$ sqlcmd -S localhost -U sa -P abcd1234$ -Q "CREATE DATABASE my_test_db;"
(3) ファイル

・Cargo.toml の編集

[dependencies] セクションに下記の内容を追加します。

[dependencies]
tiberius = { version = "0.5", features = ["chrono"] }
async-std = { version = "1.9", features = ["attributes"] }
chrono = "0.4"

ソースコード: src/main.rs

下記のソースコードをコピーして src/main.rs を書き換えます。
※ソース中の conn_str の内容は、試す環境に合わせて変更してください。

use async_std::net::TcpStream;
use tiberius::{Client, ColumnData, Config, FromSqlOwned, QueryResult};

#[async_std::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("#### Start ####");

    let conn_str = "Server=tcp:localhost,1433;TrustServerCertificate=yes;Database=my_test_db;UID=sa;PWD=abcd1234$";
    let config = Config::from_ado_string(conn_str).unwrap();
    let tcp = TcpStream::connect(config.get_addr()).await?;
    tcp.set_nodelay(true)?;
    let mut client = Client::connect(config, tcp).await?;

    println!("-- DROP & CREATE TABLE");
    client.execute("DROP TABLE IF EXISTS 会員名簿", &[]).await?;
    client
        .execute(
            "CREATE TABLE 会員名簿 (番号 int, 氏名 nvarchar(40), 誕生日 date)",
            &[],
        )
        .await?;

    println!("-- INSERT");
    let members = vec![
        (110, "岸本 龍也", "1989-11-06"),
        (210, "荒井 伸次郎", "1974-01-30"),
        (105, "江口 美奈", "1979-06-23"),
        (304, "長田 隆次", "1991-05-25"),
        (307, "中居 雄樹", "1984-02-29"),
    ];
    for (id, name, birthday) in members {
        let inserted = client
            .execute(
                "INSERT INTO 会員名簿 (番号,氏名,誕生日) VALUES (@P1, @P2, @P3)",
                &[&id, &name, &birthday],
            )
            .await?;
        assert_eq!(&[1], inserted.rows_affected());
    }
    let result = client.simple_query("SELECT * FROM 会員名簿").await?;
    display(result).await?;

    println!("-- UPDATE");
    let update_id = 307;
    let new_name = "中井 雄樹";
    let updated = client
        .execute(
            "UPDATE 会員名簿 SET 氏名=@P1 WHERE 番号=@P2",
            &[&new_name, &update_id],
        )
        .await?;
    assert_eq!(&[1], updated.rows_affected());
    let result = client
        .simple_query("SELECT 番号,氏名 FROM 会員名簿 ORDER BY 番号")
        .await?;
    display(result).await?;

    println!("-- DELETE");
    let delete_id = 210;
    let deleted = client
        .execute("DELETE FROM 会員名簿 WHERE 番号=@P1", &[&delete_id])
        .await?;
    assert_eq!(&[1], deleted.rows_affected());
    let result = client
        .simple_query("SELECT 番号,氏名,誕生日 FROM 会員名簿 ORDER BY 番号")
        .await?;
    display(result).await?;

    println!("#### Finish ####");
    Ok(())
}

async fn display(result_set: QueryResult<'_>) -> Result<(), Box<dyn std::error::Error>> {
    let cols = result_set.columns().unwrap();
    for col in cols {
        print!(" | {}", col.name());
    }
    println!(" |");

    let rows = result_set.into_first_result().await?;
    let rows_affcted = rows.len();
    let mut row_count = 0;
    for row in rows {
        for col in row {
            match col {
                ColumnData::I32(Some(v)) => print!(" | {}", v),
                ColumnData::String(Some(v)) => print!(" | {}", v),
                ColumnData::Date(_) => {
                    print!(" | {}", chrono::NaiveDate::from_sql_owned(col)?.unwrap())
                }
                _ => print!(" | {:?}", col),
            };
        }
        println!(" |");
        row_count += 1;
    }
    println!("結果 {} 行 ({})", row_count, rows_affcted);
    Ok(())
}

準備は以上です。

3. 実行結果

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 1.81s
     Running `target/debug/crud_mssql`
#### Start ####
-- DROP & CREATE TABLE
-- INSERT
 | 番号 | 氏名 | 誕生日 |
 | 110 | 岸本 龍也 | 1989-11-06 |
 | 210 | 荒井 伸次郎 | 1974-01-30 |
 | 105 | 江口 美奈 | 1979-06-23 |
 | 304 | 長田 隆次 | 1991-05-25 |
 | 307 | 中居 雄樹 | 1984-02-29 |
結果 5 行 (5)
-- UPDATE
 | 番号 | 氏名 |
 | 105 | 江口 美奈 |
 | 110 | 岸本 龍也 |
 | 210 | 荒井 伸次郎 |
 | 304 | 長田 隆次 |
 | 307 | 中井 雄樹 |
結果 5 行 (5)
-- DELETE
 | 番号 | 氏名 | 誕生日 |
 | 105 | 江口 美奈 | 1979-06-23 |
 | 110 | 岸本 龍也 | 1989-11-06 |
 | 304 | 長田 隆次 | 1991-05-25 |
 | 307 | 中井 雄樹 | 1984-02-29 |
結果 4 行 (4)
#### Finish ####

4. 補足

前述のコードでは async-std で属性によるランタイムの起動を指定してます。
初めての async-std だったのですが、async_std::task::spawn() する良い例が思い浮かばなかったので、CRUDの処理をそのまま書いてます。

この件とは別にコードを書いて試したところ、ODBC 接続を試した際に微妙だった日本語や絵文字や結合文字等は、特に問題ありませんでした。
日付型や数値型についても、それぞれ chrono, rust_decimal を features に指定すれば利用できてます。

現段階では、接続プールが実装されていないため、非同期な接続プールを利用するには、少々工夫が必要です。
※と書きましたが、tokio にして bb8 / bb8-tiberius で進める方が無難です。

何れにしても、以前より格段に書きやすく喜ばしい限りです。