ys memos

Blog

Rustでsqlxを使うメモ


rust

2025/05/13


RustからRelational DBにアクセスする際に様々なライブラリがあるが、その中でもsqlxの使用感が良かったので、メモ書きを残す。


非同期対応しており、DSLなしでコンパイル時にクエリのチェックをしてくれるクレート。 対応しているDBはPostgreSQL, MySQL, MariaDB, SQLiteとなっている。

ORMと比較されているものも多く見られるが、実際にはORMではなく、SQLをベースとし、望むならクエリチェックを行う仕組みをとっている。 感覚的には、ORMよりも直接SQLを書きたい場合のラッパーライブラリに近い。



featuresは各々の環境に合わせてであるが、このような形で Cargo.tomlに追加する。

ここでのサンプルは、MySQLとする。

sqlx = { version = "0.8", features = [
  "runtime-tokio",
  "mysql",
  "macros",
] }

Connection Poolを準備するコード。

let pool = MySqlPoolOptions::new()
    .max_connections(5)
    .connect("mysql://username:password@localhost:3306/dbname")
    .await?;

以下のサンプルコードで、これを使いまわすものとする。


sqlx::query()を使うと型の自動割当なしで、sqlx::query!()を使うと型の自動割当ありでSQLを実行できる。

まずは、基本的なSQL実行の形を紹介する。

let rows = sqlx::query("SELECT * FROM users")
    .fetch_all(&pool)
    .await?;
let row = sqlx::query("SELECT * FROM users WHERE id = ?")
    .bind("3170534137668829185")
    .fetch_one(&pool)
    .await?;

Recordが存在しない場合は、.feth_one()Resultが、Err(sqlx::Error::RowNotFound)となるので、これをチェックすることで、様々なエラーハンドリングが可能。存在しない場合にエラーを伝搬させるだけで良い場合は、?で問題ない。


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)。


ここがsqlxの大きな特徴で、記述したSQLの型を事前にチェックすることが可能。チェックするにとどまらず、マクロによって自動でその型を割り当ててくれ、上述した getによる取得や、フィールドの存在チェックを省くことが可能。

以下のコードを書いたあとに、

$ cargo sqlx prepare

## 利用DBの宛先を指定する方法
$ DATABASE_URL=mysql://username:password@localhost:3306/dbname cargo sqlx prepare

を実行し、.sqlxにSQLに対する型情報をキャッシュする。これを準備したら、sqlx::query!が型を自動で割り当ててくれ、型を手動で定義せずに、上述したコードでrow.idrow.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


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で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を準備し、そこに

.vscode/settings.json
{
  "rust-analyzer.server.extraEnv": {
    "DATABASE_URL": "mysql://username:password@localhost:3306/dbname",
  },
}

と加えることで、コードを書くごとに都度 cargo sqlx prepareを実行せずに、それ相当のことを実現可能(最終的には必要になるだろうが)。


ORMとしてはSeaORMが使用感よい印象だが、よりSQLに近い形で記述でき、かつ型チェックやフィールドの存在チェック、開発効率のための補完の支援があるsqlxは、特定の状況下においては重宝するだろう。


関連タグを探す