Rustでasync/awaitに対応したTiberiusからSQLServerに接続する
一年くらい前に Rust から ODBC ドライバで接続する方法を試してました。
その当時、直接 TDS プロトコルを扱う tiberius クレートも試してみたのですが、依存するクレートのバージョンを固定する必要があり、コードが書き辛い状態でした。
しかし、改めて確認したところ、いつの間にやら async/await に対応した tiberius クレートの開発が進んでます。
tokio だけでなく async-std にも対応しているとの事なので、今回は async-std を利用して CRUD なコードを試してみました。
1. 環境
- OS: KDE neon 5.21.4 (Ubuntu 20.04 ベース) / macOS Mojave v10.14.6
- rustc 1.51.0
- SQL Server 2019 (RTM-CU10) (KB5001090) - 15.0.4123.1 (X64) on Linux
- SQL Server Command Line Tool Version 17.7
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 で進める方が無難です。
何れにしても、以前より格段に書きやすく喜ばしい限りです。