🫡

SQLxのquery!()とquery_as!()の違いについて(Rust)

2024/02/10に公開

どもどもsouthan(サウサン)です!
絶賛Rust学習中でして、その内容をまとめさせていただきました。

今回の題材

RustのDB接続ライブラリーであるSQLxを使う機会があり、調べながらコードを書いていました。
色々記事を漁ってみると取得系はquery_as!()で更新系はquery!()とでてきましが、
どちらにどのような特徴があるか気になったので今回まとめました。

主に触れるのが

  • SQLxの特徴
  • query!()とquery_as!()の違い
    になります。

SQLxの特徴

主な特徴は以下の2点と考えています。

  • 非同期のサポート
  • コンパイル時のクエリ検証機能

非同期のサポート

githubのREADMEに以下のように書かれています。

Truly Asynchronous. Built from the ground-up using async/await for maximum concurrency.
略:async/awaitを使用してゼロから構築され、最大限の並行性を実現します。

これは特徴の一番最初に書かれています。
おそらくですが、同期的なDB操作で複数のsqlが書かれている場合、一部のsqlが重いとスレッドがアイドル状態(何もしていない状態)になってしまいます。
つまり後続の処理が止まってしまう状態です。

それに対しSQLxは非同期なので、同時に複数のSQL操作をすることが可能です。
重いsql操作があった場合でも他のsqlをアイドル状態にならずに実行することができます。
それにより効率的に処理を行うことができるのだろうと思います。

コンパイル時のクエリ検証機能

これは非同期のサポートの次に書かれている特徴です。

Compile-time checked queries (if you want)
略:コンパイル時のクエリをチェックする

つまり、Rustのコード内に記述されたSQLクエリが、実際に使用されるデータベーススキーマと互換性があることをコンパイル時に確認する仕組みです。
これにより開発の早いプロセスでSQLクエリの問題を発見し修正することができます。
開発者にとってはありがたい機能ですね。

query!()とquery_as!()の違い

本題に入らさせていただきます。

query!()に関して

早速ドキュメントを読んでみましょう(英語しかないの辛い🥲)
添付されてたコードが下です。

// let mut conn = <impl sqlx::Executor>;
let account = sqlx::query!("select (1) as id, 'Herp Derpinson' as name")
    .fetch_one(&mut conn)
    .await?;

// anonymous struct has `#[derive(Debug)]` for convenience
println!("{account:?}");
println!("{}: {}", account.id, account.name);

requirementsに

• The query must be a string literal, or concatenation of string literals using + (useful for queries generated by macro), or else it cannot be introspected (and thus cannot be dynamic or the result of another macro).

と書かれています。
つまりquery!()のメソッド内は文字列リテラルでないとダメみたいです。

// 文字列リテラルとは↓のようなもの
let hello = "Hello, world!";

また使用できるメソッドは主に5種類あるみたいです。

返すレコード数 メソッド 主な用途
0 execute insert update delete などの永続化処理
0 or 1 fetch_optional 1行より大きいデータは無視される
1のみ fetch_one 1行もデータがない場合はエラーが返却、1行より大きいデータは無視される。集計クエリはこれを使う
最低1以上 fetch 他の行を見るときは、.try_next()を使用する
複数行 fetch_all

query_as!()

次はquery_as!()についてドキュメントを見ていきましょう。
大事そうな箇所を何点かpick upしてみました。

A variant of [query!] which takes a path to an explicitly defined struct as the output type.
略:[query!] とは別の形式の、出力型として明示的に定義された構造体へのパスを取るものです。

The macro maps rows using a struct literal where the names of columns in the query are expected to be the same as the fields of the struct (but the order does not need to be the same).
略:このマクロでは、構造体リテラルを使用して行をマップします。クエリの列名は構造体のフィールドと同じである必要があります(ただし、順序は同じである必要はありません)。

The only modification to the query!() syntax is that the struct name is given before the SQL string:
略:query!()構文の唯一の変更点は、構造体名をSQL文字列の前に指定することです

以上をまとめると、

  • query_as!()は構造体へ、sqlの実行結果をマッピングする必要がある
  • コードの書き方もquery()!と違い、構造体を指定する必要がある

ということになります。

具体的なコードが載ってました。

構造体
#[derive(Debug)]
struct Account {
    id: i32,
    name: String
}

// let mut conn = <impl sqlx::Executor>;
let account = sqlx::query_as!(
    Account,  ※追記:ここで構造体にマッピングしている
    "select * from (select (1) as id, 'Herp Derpinson' as name) accounts where id = ?",
    1i32
)
.fetch_one(&mut conn)
.await?;

またquery_as!()で使用できるメソッドは4種類あるみたいです。
基本的にquery!()と変わらないですが、永続化処理は使えないみたいです。

返すレコード数 メソッド 主な用途
0 or 1 fetch_optional 1行より大きいデータは無視される
1のみ fetch_one 1行もデータがない場合はエラーが返却、1行より大きいデータは無視される。集計クエリはこれを使う
最低1以上 fetch 他の行を見るときは、.try_next()を使用する
複数 fetch_all

まとめ

上の話を要約するとquery!()とquery_as!()の違いは

  • 構造体にマッピングする必要があるか
    • query!():必要ない❌
    • query_as!():必要あり⭕️
  • 永続化の処理を実施できるか
    • query!():できる⭕️❌
    • query_as!():できない❌

になりそうです。

またSQLxのREADME には

The biggest downside to query!() is that the output type cannot be named (due to Rust not officially supporting anonymous records). To address that, there is a query_as!() macro that is mostly identical except that you can name the output type.
略:query!()の最大の欠点は、出力型に名前を付けられないことです(Rustが匿名レコードを正式にサポートしていないため)。これを解決するために、query_as!()マクロがあり、出力型に名前を付けられる以外はほとんど同じです。

と書かれています。
query!()の欠点として、出力型に名前を付けられないとのことなので基本的にはquery_as!()を使用した方がいいかもしれません。
また、query!()で呼び出した場合、adhoc structつまり一時的な構造体が割り当てられるみたいです。ここが説明の

出力型に名前を付けられない

にあたります。

ということで、今回SQLxのquery!()とquery_as!()の違いをまとめさせていただきました!
読んでいただきありがとうございました!

参考資料

https://github.com/launchbadge/sqlx
https://docs.rs/sqlx/latest/sqlx/macro.query.html
https://docs.rs/sqlx/latest/sqlx/macro.query_as.html

Discussion