はじめに
RustのDB接続用ライブラリ SQLx を使っているのですが、query
とquery_as
の仕様や両者の違い、使い分けが曖昧だったので整理しました。
TL;DR
query
とquery_as
関数にはそれぞれマクロ版もあるので以下4パターンになる。
function
function | 動的/静的SQL | コンパイル時チェック | 構造体へのマッピング | 用途 |
---|---|---|---|---|
query |
動的 | なし | 手動 | 柔軟性が必要な動的クエリ |
query_as |
動的 | なし | 自動(指定構造体) | 結果を指定構造体にマッピングする動的クエリ |
動的なSQLが必要な場合はquery
やquery_as
を使う。
柔軟性は高いが、型安全性は低い。
macro
macro | 動的/静的SQL | コンパイル時チェック | 構造体へのマッピング | 用途 |
---|---|---|---|---|
query! |
静的 | あり | 自動(匿名構造体) | 型安全性が必要な静的クエリ |
query_as! |
静的 | あり | 自動(指定構造体) | 型安全性と指定構造体へのマッピングが必要な静的クエリ |
静的なSQLで型安全性が必要な場合はquery!
やquery_as!
を使う。柔軟性は低いが、型安全性は高い。
なお、マクロを使えばコンパイル時に以下のチェックを行ってくれる。
- SQLの構文チェック
- DBスキーマとの整合性チェック
- 型チェック
コンパイル時チェックでは、DBにアクセスしてスキーマ定義の解析が行われるため、コンパイル時にDBへのリーチャビリティがなければエラーとなる。
ただし、.sqlx
ファイルを使えばオフラインでもコンパイル時チェックが可能。
具体的なコード例と使い分けは以下。
query
動的なSQLクエリを実行する際に利用する。
『動的』であることから、コンパイラは構文や型のチェックができないので安全性は低い。
let dynamic_column = "email"; // WHERE句のField名がユーザー入力によって変わるとか
let user_id = 1;
let user_info: (String,) = sqlx::query(
&format!("SELECT {} FROM users WHERE id = $1", dynamic_column)
)
.bind(user_id)
.fetch_one(&pool)
.await
.expect("Failed to fetch user")
.try_get(0)
.expect("Failed to get column value");
println!("User {}: {}", user_id, user_info.0);
戻り値の型
Query<'q, DB, <DB as HasArguments<'q>>::Arguments>
Query
オブジェクトは fetch
や execute
などのメソッドを持っている。これらを呼び出すことで実際のデータベース操作が行える。
適用ケース
動的なWHERE句などを持つクエリを実行したい場合。
query!
query
のマクロ版。静的なSQLクエリを実行する際に利用する。
コンパイル時チェックが行われるので安全。
let user_id = 1;
let user = sqlx::query!("SELECT id, name FROM users WHERE id = $1", user_id)
.fetch_one(&pool)
.await?;
println!("User {}: {}", user.id, user.name);
戻り値の型
コンパイラが生成する匿名の構造体。
具体的には、query!
マクロはDBのスキーマ定義に基づいた匿名構造体を生成してくれる。
上記の例でいうと user
は概念的に以下のような匿名構造体を持つ。
struct AnonymousUser {
id: i32,
name: String,
}
適用ケース
SQLクエリ結果をマッピングするための構造体を使わず、静的なクエリを実行して結果を型安全に取得したい場合。
query_as
query
の拡張版であり、SQL実行結果を指定した構造体にマッピングしてくれる。
ただし、マッピング対象の構造体にsqlx::FromRow
をderiveする必要がある。sqlx::FromRow
は内部的にSQL実行結果のレコードを構造体オブジェクトにデシリアライズしてくれる。
なお、query_as
はコンパイル時のチェックはされないので、例えばマッピング対象の構造体とクエリの実行結果の型が不一致だったとしてもエラーは検出できない点に注意。
#[derive(sqlx::FromRow)]
struct User {
id: i32,
name: String,
email: String,
}
let user_id = 1;
let user = sqlx::query_as::<_, User>("SELECT id, name, email FROM users WHERE id = $1")
.bind(user_id)
.fetch_one(&pool)
.await?;
println!("User {}: {} ({})", user.id, user.name, user.email);
戻り値の型
QueryAs<'q, DB, T, <DB as HasArguments<'q>>::Arguments>
T
は指定した構造体の型になる。
適用ケース
- クエリ結果を事前に定義した構造体に直接マッピングしたい場合
- 動的なSQLクエリ(実行時に構築されるクエリ)を使用する場合
- CI環境やオフライン開発時に「DBへの接続はできないけどコンパイルを通したい」場合
query_as!
query!
とquery_as
の組み合わせで、コンパイル時のチェックと構造体へのマッピングを同時に行える。
静的なSQLクエリを実行する際に利用する。
struct User {
id: i32,
name: String,
email: String,
}
let user_id = 1;
let user = sqlx::query_as!(User, "SELECT id, name, email FROM users WHERE id = $1", user_id)
.fetch_one(&pool)
.await?;
println!("User {}: {} ({})", user.id, user.name, user.email);
戻り値の型
指定した構造体型 T
を返す Query
オブジェクト。
適用ケース
- クエリ結果を事前に定義した構造体に直接マッピングしたい場合
- コンパイル時チェックを行いたい場合
query_as!
マクロを使用する場合、構造体に#[derive(sqlx::FromRow)]
を付ける必要はない。自動的に必要な実装(フィールドの値のデシリアライズなど)が生成される。
おわりに
可能な限りマクロ(query!
, query_as!
)を使って型安全性を担保するのがいいですね。
参考リンク