Open6

sqlx で列を更新しない場合とNULLに更新する場合を使い分ける

koko_ukoko_u

sqlx の使い方のメモ

  • クエリのパラメータとして渡す型は Type トレイトを実装している必要がある
    • データベースの種類に応じて、良く知られた型に対ししては既に実装されている
    • Json もパラメータに渡せる
    • PostgreSQL の場合、渡された Json<T>Json 型に見える
koko_ukoko_u

更新対象となるデータが次のような形式だとすると、Noneの場合に列を更新したくないのか、NULLに更新したいのか区別がつかない

pub struct UpdateUser {
    pub id: Uuid,
    pub name: Option<String>,
    pub profile: Option<String>,
}
koko_ukoko_u

Option の代わりにシリアライズした結果、SQL側で NULL でも更新したいか、更新したくないかを判断できる情報を渡せるような enum を自前で作る

#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", content = "value")]
#[serde(rename_all = "lowercase")]
pub enum Updator<T> {
    NoUpdates,
    Update(Option<T>),
}

serde は enum をシリアライズする時に色々な形式で変換できて、上記のようにすると Updator::NoUpdates

{
    "type": "noupdates"
}

となり、Updator::Update(None)

{
    "type": "updates",
   "value": null
}

のように、SQL に渡した時に使いやすい形になる

koko_ukoko_u

あとは UPDATE 文で CASE による分岐を書くだけ

let user = sqlx::query_as::<_, User>(
        r#"
        UPDATE users
        SET
            name = CASE $2 ->> 'type'
                    WHEN 'noupdates' THEN name
                    WHEN 'update' THEN $2 ->> 'value'
                END
        , profile = CASE $3 ->> 'type'
                        WHEN 'noupdates' THEN profile
                        WHEN 'update' THEN $3 ->> 'value'
                    END
        WHERE
            id = $1
        RETURNING
            id
        , name
        , profile
        "#,
    )
    .bind(id)
    .bind(Json::from(Updator::Update("John".into())))
    .bind(Json::from(Updator::NoUpdates))
    .fetch_optional(pool)
    .await
koko_ukoko_u

上記は PostgreSQL の想定。MySQL の場合などデータベースエンジンに応じてクエリは変わる。知らんけど。

koko_ukoko_u

マクロで実行するには、もっと型を合わせないと難しい。

  • SQL でパラメータに json の型ヒントを書く
  • Json<T> ではなく JsonValue でパラメータを渡す
UPDATE users
SET
  name = CASE $2::json ->> 'type'
    WHEN 'noupdates' THEN name
    WHEN 'update' THEN $2::json ->> 'value'
  END
, profile = CASE $3::json ->> 'type'
    WHEN 'noupdates' THEN profile
    WHEN 'update' THEN $3::json ->> 'value'
  END
WHERE
  id = $1
RETURNING
  id
, name
, profile;
 let user = sqlx::query_file_as!(
     User,
     "queries/update_user.sql",
     id,
     json!(Updator::Update("John".into())),
     json!(Updator::NoUpdates)
)
.fetch_optional(pool)
.await
.change_context(AppError)?;