Open8

Rust の sqlx 使い方メモ

koko_ukoko_u

SELECT した結果が NOT NULL であることを伝える

SELECT
    U.id AS user_id,
    U.name AS user_name,
    R.code AS role_code
FROM
    users AS U
    LEFT OUTER JOIN
    roles AS R
    ON U.role_id = R.role_id

のような検索で、user_iduser_name が NULL でないとしても、このまま sqlx で実行するとうまくいかない

struct User {
    user_id: i32,
    user_name: String,
    role_code: Option<String>,
}
~~~~
sqlx::query_file_as!(User, "select_users.sql").fetch_many(&pool).await;

SQL 文で NOT NULL であることをヒントとして感嘆符マークで与えなければならない

SELECT
    U.id AS "user_id!",
    U.name AS "user_name!",
    R.code AS role_code
FROM
    users AS U
    LEFT OUTER JOIN
    roles AS R
    ON U.role_id = R.role_id
koko_ukoko_u

JSON 型のカラムを受け取る

Postgres で用意されている JSON 型を sqlx で行データとして受け取りたい

{ "a": 42, "b": "hello" } のような値を JSON 型として持つ列を取得するために、列 col の型として、sqlx::types::Json<T> を指定する

#[derive(Debug, Serialize, Deserialize)]
struct ColData {
    a: i32,
    b: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct RowData {
    col: sqlx::types::Json<ColData>,
}

SQL を実行する時に、列 col の型を明示する。NULL でないことを示すために ! も付ける

  let row = sqlx::query_as!(
        RowData,
        r#"
         WITH vars (col) AS (SELECT '{ "a": 100, "b": "hello" }'::json)
         SELECT col AS "col!: sqlx::types::Json<ColData>" FROM vars
         "#
    )
    .fetch_one(conn.as_mut())
    .await
koko_ukoko_u

カラムが NULL許容の場合は、RowData のフィールドの定義を Option にする

#[derive(Debug, Serialize, Deserialize)]
struct RowData {
    col: Option<sqlx::types::Json<ColData>>,
}

SQL で表明する列の型に Option は不要。! だけ除く

let row = sqlx::query_as!(
        RowData,
        r#"
         WITH vars (col) AS (SELECT '{ "a": 100, "b": "hello" }'::json)
         SELECT col AS "col: sqlx::types::Json<ColData>" FROM vars
         "#
    )
    .fetch_one(conn.as_mut())
    .await
koko_ukoko_u

カラムにデータベースの意味での NULL が格納されている場合と、JSON の null が非NULL値として格納されている場合は区別されるので注意する

let row = sqlx::query_as!(
        RowData,
        r#"
         WITH vars (col) AS (SELECT NULL::json)
         SELECT col AS "col: sqlx::types::Json<ColData>" FROM vars
         "#
    )
    .fetch_one(conn.as_mut())
    .await

は正常に処理されて RowData { col: None } を得るが

let row = sqlx::query_as!(
        RowData,
        r#"
         WITH vars (col) AS (SELECT 'null'::json)
         SELECT col AS "col: sqlx::types::Json<ColData>" FROM vars
         "#
    )
    .fetch_one(conn.as_mut())
    .await

はパニックする

koko_ukoko_u

1 対 n の結合を取得する

JSON で取得することで、例えばブログの 1投稿に対して、複数のコメントを Vec<Comment> のような形式で取得できる

テーブルの例

ER図

SQL

詳細
WITH
    posts_comment (
        post_id
    ,   comment_array
    ) AS (
        SELECT
            posts.id
        ,   CASE
                WHEN MAX(comments.id) IS NULL THEN ARRAY[]::json[]
                ELSE ARRAY_AGG(
                    JSON_BUILD_OBJECT(
                        'comment_id', comments.id,
                        'comment_user', comments.username,
                        'comment', comments.comment
                    )
                )
            END
        FROM
            posts
            LEFT OUTER JOIN
            comments
            ON
                posts.id = comments.post_id
        GROUP BY
            posts.id
    )

SELECT
    posts.id
  , posts.title
  , posts.username
  , posts.content
  , posts_comment.comment_array
FROM
    posts
    LEFT OUTER JOIN
    posts_comment
    ON
        posts.id = posts_comment.post_id
  • 1つの投稿に対して、複数のコメントをARRAY_AGG 関数で集約する
  • 配列の要素となるJSONを JSON_BUILD_OBJECT でコメント1行をJSONに変換する
  • 投稿に対するコメントが存在しない場合に対して空の配列を作成するために、comments.id が NULL かどうか CASE 文で判定する

sqlx のためにカラムのエイリアスを調整する

DIFF
   , posts.title
   , posts.username
   , posts.content
-  , posts_comment.comment_array
+  , posts_comment.comment_array AS "comments!: Vec<sqlx::types::Json<CommentModel>>"
 FROM
     posts
     LEFT OUTER JOIN posts_comment ON posts.id = posts_comment.post_id

受け取るデータ型を用意する

型定義
#[derive(Debug, Serialize, Deserialize)]
pub struct CommentModel {
    pub comment_id: i32,
    pub comment_user: String,
    pub comment: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PostModel {
    pub id: i32,
    pub title: String,
    pub username: String,
    pub content: String,
    pub comments: Vec<sqlx::types::Json<CommentModel>>,
}
  • SQL の SELECT 節で型を指定した comments に合わせて、PostModel 型のフィールドの型を指定する

sqlx::query_as! に渡して得られるデータ例

データ例
[
    PostModel {
        id: 1,
        title: "Getting started Rust",
        username: "john",
        content: "Make it so, devastation! Pathways fly with voyage!",
        comments: [
            Json(
                CommentModel {
                    comment_id: 1,
                    comment_user: "yamada",
                    comment: "Prarere saepe ducunt ad clemens fiscina.",
                },
            ),
            Json(
                CommentModel {
                    comment_id: 2,
                    comment_user: "sato",
                    comment: "Nobilis aonidess ducunt ad equiso.",
                },
            ),
            Json(
                CommentModel {
                    comment_id: 3,
                    comment_user: "hako",
                    comment: "Vae, flavum nutrix!",
                },
            ),
        ],
    },
    PostModel {
        id: 2,
        title: "Deep Dive Rust",
        username: "ken",
        content: "Kahlesses walk from galaxies like brave spacecrafts.",
        comments: [
            Json(
                CommentModel {
                    comment_id: 4,
                    comment_user: "minu",
                    comment: "Est gratis amicitia, cesaris.",
                },
            ),
        ],
    },
    PostModel {
        id: 3,
        title: "Web app for Rust",
        username: "jammy",
        content: "Evasive parasites, to the infinity room.",
        comments: [],
    },
]

koko_ukoko_u

New Type パターンを使うには

例えば、todos テーブルの主キー id BIGSERIAL のカラムを単純に i64 ではなく、TodoId 型としたい。

todos テーブル
CREATE TABLE todos (
    id BIGSERIAL NOT NULL PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    description TEXT NULL,
    completed BOOLEAN NOT NULL DEFAULT(false)
)
対応する struct
pub struct TodoId(i64);

pub struct Todo {
    pub id: TodoId,
    pub title: String,
    pub description: Option<String>,
    pub completed: bool
}

TodoId をクエリで使うには

  • パラメータとして渡せる
  • クエリの戻り値として受け取れる

必要がある

koko_ukoko_u

まずは後者の対応をメモする

結論から

#[derive(sqlx::TYpe)]
#[sqlx(transparent)]
pub TodoId(pub i64);

とする。このへんを参照する。

コンパイルエラーは sqlx::Decode<'r, DB> が実装されていないと出るので注意が必要

koko_ukoko_u

これで、query_as などの関数にパラメータを bind できるし、Todo を戻り値にできる

let todo: Option<Todo> = sqlx::query_as("SELECT * FROM todos WHERE id = $1").bind(todo_id).fetch_optional(&mut conn).await?;

ただし query_as!マクロには適用できない。

  • パラメータとしての渡し方はわからん。
  • 戻り値は i64 から TodoId への From トレイトを実装することで対応できそう