2025/05/13
はじめに
RustからRelational DBにアクセスする際に様々なライブラリがあるが、その中でもsqlxの使用感が良かったので、メモ書きを残す。
sqlxの特徴
非同期対応しており、DSLなしでコンパイル時にクエリのチェックをしてくれるクレート。 対応しているDBはPostgreSQL, MySQL, MariaDB, SQLiteとなっている。
ORMと比較されているものも多く見られるが、実際にはORMではなく、SQLをベースとし、望むならクエリチェックを行う仕組みをとっている。 感覚的には、ORMよりも直接SQLを書きたい場合のラッパーライブラリに近い。
使い方
depsに追加
featuresは各々の環境に合わせてであるが、このような形で Cargo.toml
に追加する。
ここでのサンプルは、MySQLとする。
sqlx = { version = "0.8", features = [
"runtime-tokio",
"mysql",
"macros",
] }
Connection Poolを準備
Connection Poolを準備するコード。
let pool = MySqlPoolOptions::new()
.max_connections(5)
.connect("mysql://username:password@localhost:3306/dbname")
.await?;
以下のサンプルコードで、これを使いまわすものとする。
SQL実行の形
sqlx::query()
を使うと型の自動割当なしで、sqlx::query!()
を使うと型の自動割当ありでSQLを実行できる。
まずは、基本的なSQL実行の形を紹介する。
複数のRecordを取得する場合
let rows = sqlx::query("SELECT * FROM users")
.fetch_all(&pool)
.await?;
単一のRecordを取得する場合
let row = sqlx::query("SELECT * FROM users WHERE id = ?")
.bind("3170534137668829185")
.fetch_one(&pool)
.await?;
Recordが存在しない場合は、.feth_one()
のResult
が、Err(sqlx::Error::RowNotFound)
となるので、これをチェックすることで、様々なエラーハンドリングが可能。存在しない場合にエラーを伝搬させるだけで良い場合は、?
で問題ない。
型チェック無しでSQLを実行
引数なし
SQL実行と、recordのフィールドへのアクセスの例がこちら。
型指定はフィッシュ構文でも型推論でもどちらでも良く、文字列は&str
でもString
でも良い。
let sql = "SELECT * FROM users";
let rows = sqlx::query(sql).fetch_all(&pool).await?;
if rows.is_empty() {
println!("No rows found");
return Ok(());
}
for row in rows {
let id = row.get::<i64, _>("id"); // キーが存在しないか、i64に変換できない場合、panicとなる
// let id: i64 = row.try_get("id")?; // このように、型推論に頼ることも可能
let name: &str = row.try_get::<&str, _>("name")?; // キーが存在しない、あるいは変換ができない場合、Errを返す
// let name: String = row.try_get("name")?; // 型推論でもよいし、`String`でもよい
println!("id: {}, name: {}", id, name);
}
引数あり
SQLを用いて、SQLに入力を渡してを実行する場合は、placeholderとbind
メソッドを使う。
let sql = "SELECT * FROM users WHERE id = ?";
let rows = sqlx::query(sql)
.bind("3170534137668829185")
.fetch_all(&pool)
.await?;
if rows.is_empty() {
println!("No rows found");
return Ok(());
}
for row in rows {
let id = row.get::<i64, _>("id");
let name = row.try_get::<&str, _>("name")?;
println!("id: {}, name: {}", id, name);
}
PostgreSQLの場合は、$1
のようにplaceholderを指定するとのこと(ref)。
型チェック有りでSQLを実行
ここがsqlxの大きな特徴で、記述したSQLの型を事前にチェックすることが可能。チェックするにとどまらず、マクロによって自動でその型を割り当ててくれ、上述した get
による取得や、フィールドの存在チェックを省くことが可能。
以下のコードを書いたあとに、
$ cargo sqlx prepare
## 利用DBの宛先を指定する方法
$ DATABASE_URL=mysql://username:password@localhost:3306/dbname cargo sqlx prepare
を実行し、.sqlx
にSQLに対する型情報をキャッシュする。これを準備したら、sqlx::query!
が型を自動で割り当ててくれ、型を手動で定義せずに、上述したコードでrow.id
やrow.name
のように、フィールドにアクセスできるようになる。
引数なし
sqlx::query!
の場合、let sql = "..."
のように、SQLを変数に格納することはできず、SQLを直接書く必要がある。
let rows = sqlx::query!("SELECT * FROM users").fetch_all(&pool).await?;
if rows.is_empty() {
println!("No rows found");
return Ok(());
}
for row in rows {
println!("id: {}, name: {}", row.id, row.name);
}
$ DATABASE_URL=mysql://username:password@localhost:3306/dbname cargo sqlx prepare
引数あり
let rows = sqlx::query!("SELECT * FROM users WHERE id = ?", "3170534137668829185")
.fetch_all(&pool)
.await?;
if rows.is_empty() {
println!("No rows found");
return Ok(());
}
for row in rows {
println!("id: {}, name: {}", row.id, row.name);
}
$ DATABASE_URL=mysql://username:password@localhost:3306/dbname cargo sqlx prepare
tips
COUNTがcheckできない
COUNT
だけにとどまらず、他の集約関数も同様の問題が発生する可能性がある。
以下のようなコードで、
let row = sqlx::query!("SELECT COUNT(id) FROM users")
.fetch_one(&pool)
.await?;
cargo sqlx prepare
をすると、
column name "COUNT(id)" is invalid: "COUNT(id)" is not a valid Rust identifierrustcClick for full compiler diagnostic
というエラーとなる。この対処のためには、
let row = sqlx::query!("SELECT COUNT(id) AS cnt FROM users")
.fetch_one(&pool)
.await?;
といったように、AS
を使って、SQLのカラム名を変更するとよい。
vscodeでsqlxの即時補完を有効にする
VSCodeでrust-analyzerを使っている場合、エディタによって cargo sqlx prepare
相当のことを内部で実現可能。
何も設定せずに cargo sqlx prepare
も実行しないで sqlx::query!
にクエリを書くと、
set `DATABASE_URL` to use query macros online, or run `cargo sqlx prepare` to update the query cacherustcClick for full compiler diagnostic
とエラーが出る。これは、onlineモードでのクエリチェック上のエラーで、rust-analyzerの実行時に、有効なDATABASE_URL
を持っていると良い。
そこで、 .vscode/settings.json
を準備し、そこに
{
"rust-analyzer.server.extraEnv": {
"DATABASE_URL": "mysql://username:password@localhost:3306/dbname",
},
}
と加えることで、コードを書くごとに都度 cargo sqlx prepare
を実行せずに、それ相当のことを実現可能(最終的には必要になるだろうが)。
おわりに
ORMとしてはSeaORMが使用感よい印象だが、よりSQLに近い形で記述でき、かつ型チェックやフィールドの存在チェック、開発効率のための補完の支援があるsqlxは、特定の状況下においては重宝するだろう。