🧰

Rust | SQLx の macro `query!` と `query_as!` を使いこなす

2023/09/25に公開

SQLx クレート

Rust のデーベース接続のライブラリである SQLx を使っていきます🧰

https://github.com/launchbadge/sqlx

SQLx の特徴

SQLx の特徴の 1 つに ORM ではないこと が挙げられます。

この ORM ではないこと が私のお気に入りの理由です。

複雑な設計やサービスになってくればなるほど、生の SQL を書く機会が増えるので、
それなら最初から書きたいと思ってしまうのです...

あと、単純にマッパーで実装するのが好きじゃない...笑
(ここは賛否両論あるでしょう🙏)

SQLx 上に構築された ORM

SQLx 上に構築された ORM が存在します。
ぜひ、チェックして見てください!

Macro か Function か

SQLx にはクエリを記述する方法がいくつかあり、
よく使用するものとして、queryquery_as があります。

query_as では返り値の型を指定できますが、query ではできません。

そして、それぞれマクロ版と関数版があります。

マクロの場合、コンパイル時にフィールドの有無や型のチェックがおこなわれるので、
個人的にはマクロを使用するのがオススメしたいです。

Macro

Function

query! か query_as! か

以下のような基準で使い分けるのが、良いのかなと思っています。

  • 取得(SELECT)系 → query_as!
  • 更新(INSERT、UPDATE、DELETE)系 → query!
    • ただし、RETURNING 句を使う場合は query_as! がオススメ

query! と query_as! の実装サンプル

データベースの準備

テーブルとサンプルデータは以下のクエリで作成しました。

create table if not exists mountains (
    id serial primary key,
    "name" varchar not null,
    created_at timestamp not null default now(),
    updated_at timestamp not null default now()
);

insert into
    mountains ("name")
values
    ('富士山'),
    ('高尾山'),
    ('大雪山');

クレートと .env の準備

非同期処理ランタイムには tokio を使います。

cargo add sqlx --features "postgres runtime-tokio-native-tls chrono"
cargo add tokio --features=full
cargo add dotenv

データベースの接続 URL を定義しておきます。

.env
DATABASE_URL = postgres://postgres:postgres@localhost:5432/sample_db

query_as!

query_as! で、単純な SELECT を実装してみました。
SQL の知識があれば、難なく読み解けるのではないでしょうか。

main.rs
use sqlx::postgres::PgPoolOptions;
use sqlx::types::chrono::NaiveDateTime;
use sqlx::{Pool, Postgres};
use std::env;

#[tokio::main]
async fn main() {
    // 環境変数を読み取り、URLを生成
    dotenv::dotenv().expect("Failed to read .env file");
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");

    // DBコネクションを生成
    let db = PgPoolOptions::new()
        .max_connections(10)
        .connect(&database_url)
        .await
        .unwrap_or_else(|_| panic!("Cannot connect to the database"));

    match get_one(&db, 1).await {
        Ok(m) => println!("{:?}", m),
        Err(e) => println!("{:?}", e),
    }
}

#[derive(Debug)]
struct Mountain {
    id: i32,
    name: String,
    created_at: NaiveDateTime,
    updated_at: NaiveDateTime,
}

async fn get_one(db: &Pool<Postgres>, id: i32) -> Result<Mountain, sqlx::Error> {
    sqlx::query_as!(
        Mountain,
        r#"
            select
                id,
                name,
                created_at,
                updated_at
            from
                mountains
            where
                id = $1
        "#,
        id
    )
    .fetch_one(db)
    .await
}

試しに name の型を i32 に変更して、コンパイルしてみます。

以下のようなエラーが出ました。
コンパイル時にチェックできる👏✨

error[E0277]: the trait bound `i32: From<std::string::String>` is not satisfied
  --> src/main.rs:32:5
   |
32 | /     sqlx::query_as!(
33 | |         Mountain,
34 | |         r#"
35 | |             select
...  |
45 | |         id
46 | |     )
   | |_____^ the trait `From<std::string::String>` is not implemented for `i32`

query_as()

関数版でも実装してみました。

こちらも、単純な SELECT 文です。
SQL の知識があれば、難なく読み解けるのではないでしょう。

引数が多くなると、bind が増えていきます。
ORM に慣れている方にとっては、こちらの方が馴染みがあるかもしれません。

また、データをマップする構造体に FromRow を derive する必要があります。

main.rs
use sqlx::postgres::PgPoolOptions;
use sqlx::types::chrono::NaiveDateTime;
use sqlx::{FromRow, Pool, Postgres};
use std::env;

#[tokio::main]
async fn main() {
    // 環境変数を読み取り、URLを生成
    dotenv::dotenv().expect("Failed to read .env file");
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");

    // DBコネクションを生成
    let db = PgPoolOptions::new()
        .max_connections(10)
        .connect(&database_url)
        .await
        .unwrap_or_else(|_| panic!("Cannot connect to the database"));

    match get_one(&db, 1).await {
        Ok(m) => println!("{:?}", m),
        Err(e) => println!("{:?}", e),
    }
}

// FromRow を追加する必要がある
#[derive(FromRow, Debug)]
struct Mountain {
    id: i32,
    name: String,
    created_at: NaiveDateTime,
    updated_at: NaiveDateTime,
}

async fn get_one(db: &Pool<Postgres>, id: i32) -> Result<Mountain, sqlx::Error> {
    sqlx::query_as::<_, Mountain>(
        r#"
            select
                id,
                name,
                created_at,
                updated_at
            from
                mountains
            where
                id = $1
        "#,
    )
    .bind(id)
    .fetch_one(db)
    .await
}

試しに name の型を i32 に変更して、コンパイルしてみます。

今回は実行時に以下のようなエラーが出ました。

ColumnDecode { index: "\"name\"", source: "mismatched types; Rust type `i32` (as SQL type `INT4`) is not compatible with SQL type `VARCHAR`" }

コンパイル時にエラーになる方が親切だなぁと感じます😉😉

query!

query! で INSERT を実装してみました。
これも、SQLの知識があれば、難なく読み解けるのではないでしょう。

main.rs
use sqlx::postgres::{PgPoolOptions, PgQueryResult};
use sqlx::types::chrono::NaiveDateTime;
use sqlx::{Pool, Postgres};
use std::env;

#[tokio::main]
async fn main() {
    // 環境変数を読み取り、URLを生成
    dotenv::dotenv().expect("Failed to read .env file");
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");

    // DBコネクションを生成
    let db = PgPoolOptions::new()
        .max_connections(10)
        .connect(&database_url)
        .await
        .unwrap_or_else(|_| panic!("Cannot connect to the database"));

    match create(&db, "比叡山").await {
        Ok(res) => println!("{:?}", res),
        Err(e) => println!("{:?}", e),
    }
}

#[derive(Debug)]
struct Mountain {
    id: i32,
    name: String,
    created_at: NaiveDateTime,
    updated_at: NaiveDateTime,
}

async fn create(db: &Pool<Postgres>, name: &str) -> Result<PgQueryResult, sqlx::Error> {
    sqlx::query!(
        r#"
            insert into
                mountains ("name")
            values
                ($1)
        "#,
        name.to_string()
    )
    .execute(db) // insert なので fetch_one ではなく execute
    .await
}

実行結果は以下は出力されます。
query!execute() では、更新された行の数を結果として取得できます。

PgQueryResult { rows_affected: 1 }

RETURNING 句 と query_as!

INSERT、UPDATE、DELETE の各コマンドで、更新した行データをそのまま取得したい場合に、
RETURNING 句を使うことができます。

その際、query_as! で実装することで型の指定ができます!

main.rs
use sqlx::postgres::{PgPoolOptions};
use sqlx::types::chrono::NaiveDateTime;
use sqlx::{Pool, Postgres};
use std::env;

#[tokio::main]
async fn main() {
    // 環境変数を読み取り、URLを生成
    dotenv::dotenv().expect("Failed to read .env file");
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");

    // DBコネクションを生成
    let db = PgPoolOptions::new()
        .max_connections(10)
        .connect(&database_url)
        .await
        .unwrap_or_else(|_| panic!("Cannot connect to the database"));

    match create(&db, "比叡山").await {
        Ok(m) => println!("{:?}", m),
        Err(e) => println!("{:?}", e),
    }
}

#[derive(Debug)]
struct Mountain {
    id: i32,
    name: String,
    created_at: NaiveDateTime,
    updated_at: NaiveDateTime,
}

async fn create(db: &Pool<Postgres>, name: &str) -> Result<Mountain, sqlx::Error> {
    sqlx::query_as!(
        Mountain,
        r#"
            insert into
                mountains ("name")
            values
                ($1)
            returning
                id,
                name,
                created_at,
                updated_at
        "#,
        name.to_string()
    )
    .fetch_one(db)
    .await
}

SQLx の実装例

公式のリポジトリに多くの examples があります。
非常に参考になるので、ぜひチェックしてみてください

https://github.com/launchbadge/sqlx/tree/main/examples

まとめ

SQLx は扱いやすく、お気に入りのクレートです。

今日はこれだけ覚えて帰ってください🥰

  • 取得(SELECT)系 → query_as!
  • 更新(INSERT、UPDATE、DELETE)系 → query!
    • ただし、RETURNING 句を使う場合は query_as! がオススメ

SQL ファイルを分離して管理できる query_file!query_file_as! もあるので、
また触ってみたいと思います!!

追記

触ってみました!!!

https://zenn.dev/collabostyle/articles/ee8ae904453c18

参考

コラボスタイル Developers

Discussion