2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

RustのDB接続用ライブラリ SQLx を使っているのですが、queryquery_as の仕様や両者の違い、使い分けが曖昧だったので整理しました。

TL;DR

queryquery_as関数にはそれぞれマクロ版もあるので以下4パターンになる。

function

function 動的/静的SQL コンパイル時チェック 構造体へのマッピング 用途
query 動的 なし 手動 柔軟性が必要な動的クエリ
query_as 動的 なし 自動(指定構造体) 結果を指定構造体にマッピングする動的クエリ

動的なSQLが必要な場合はqueryquery_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オブジェクトは fetchexecute などのメソッドを持っている。これらを呼び出すことで実際のデータベース操作が行える。

適用ケース

動的な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!)を使って型安全性を担保するのがいいですね。

参考リンク

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?