SQLxのUuid型はSerializeできない

SQLxのUuid型とuuidクレートのUuidの違い

Posted on Tuesday, May 17, 2022

TOC

sqlx::types::Uuid vs uuid::Uuid

RustでDBに接続するための有用なクレートとしてSQLxがある。 かつてはDieselが有名だったが、async対応ができてないなどの理由から新興のSQLxの方が今は人気がある。

そんなSQLxだが、SQLxクレートがDBに対応した型を一部提供してくれている。その中にUuid型があるのだが、これが結構クセモノだったりする。

というのも、Webアプリケーションを作成するときはDBから得られたUuid型のデータをJSONとして変換してクライアント側に返却するというのはよくあることだが、SQLxが提供するUuid型はSerializeが実装されていないので構造体をそのままJSONに変換することができない。

以下のコードを考えてみる。

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use sqlx::{postgres::PgRow, Pool, Postgres, Row};
//use uuid::Uuid;

#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)]
pub struct UserAccessToken {
    pub user_id: i64,
    pub access_token: sqlx::types::Uuid,
}

pub fn register_user(id: i64) -> Result<UserAccessToken> {
  sqlx::query(
  r#"
    INSERT INTO users (id) VALUES ($1)
    RETURNING id, access_token
  "#,
  )
  .bind(some_id)
  .map(|row: PgRow| UserAccessToken {
      user_id: row.get(0),
      //access_token: Uuid::from_u128(row.get::<sqlx::types::Uuid, _>(1).as_u128()),
      access_token: row.get(1),
  })
  .fetch_one(pool)
  .await
  .context("Creating user")
}

SQLxでクエリを発行する際はquery_asが有用だが、今回は明示的に型変換を行うためqueryを使っている。

このコードで肝となるのは.map(|row| UserAccessToken { ... })の部分で、構造体としてaccess_tokenフィールドはsqlx::types::Uuid型を指定している。

このコードはコンパイルは通らず、エラーメッセージは

the trait bound `sqlx::types::Uuid: Deserialize<'_>` is not satisfied
the trait `Deserialize<'_>` is not implemented for `sqlx::types::Uuid`

を吐く。 これはsqlx::types::UuidSerialize/Deserializeを実装していないからである。

ではaccess_tokenフィールドの型をuuid::Uuidとし、.map(...)の中はそのままにしてみる。 このときuuidクレートのfeaturesにserdeを入れるのを忘れずに。

#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)]
pub struct UserAccessToken {
    pub user_id: i64,
    pub access_token: uuid::Uuid,
}

pub fn register_user(id: i64) -> Result<UserAccessToken> {
  sqlx::query(
  r#"
    INSERT INTO users (id) VALUES ($1)
    RETURNING id, access_token
  "#,
  )
  .bind(some_id)
  .map(|row: PgRow| UserAccessToken {
      user_id: row.get(0),
      //access_token: Uuid::from_u128(row.get::<sqlx::types::Uuid, _>(1).as_u128()),
      access_token: row.get(1),
  })
  .fetch_one(pool)
  .await
  .context("Creating user")
}

これはどうなるかというと、これもコンパイルが通らない。 エラーメッセージは以下の通り。

the trait bound `uuid::Uuid: sqlx::Decode<'_, Postgres>` is not satisfied
the trait `sqlx::Decode<'_, Postgres>` is not implemented for `uuid::Uuid`

uuidクレートのUuid型はSQLxが提供するDecodeを実装してないためである。

JSONにデシリアライズすることはできてもDBからデータを構造体にシリアライズできない。

ということで折衷案として以下のように、DBからデータの受け渡しはsqlx::types::Uuidで受け、それを一度u128に変換し、これをuuid::Uuidに持っていく必要がある。

#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)]
pub struct UserAccessToken {
    pub user_id: i64,
    pub access_token: uuid::Uuid,
}

pub fn register_user(id: i64) -> Result<UserAccessToken> {
  sqlx::query(
  r#"
    INSERT INTO users (id) VALUES ($1)
    RETURNING id, access_token
  "#,
  )
  .bind(some_id)
  .map(|row: PgRow| UserAccessToken {
      user_id: row.get(0),
      access_token: uuid::Uuid::from_u128(row.get::<sqlx::types::Uuid, _>(1).as_u128()),
  })
  .fetch_one(pool)
  .await
  .context("Creating user")
}

本来ならもっと上手いやり方がありそうな気もするのだが、今のところはこのような方法で乗り切っている。