postgresql 正确处理Rust和SQLx中的层次结构

kcugc4gi  于 2023-06-22  发布在  PostgreSQL
关注(0)|答案(1)|浏览(276)

我正在使用actix-web,SQLx和PostgreSQL编写一个用Rust编写的REST API用于存储。让我们假设这是我的schema(表示为Rust结构):

struct User {
    pub id: Uuid,
    pub email: String
    // And so on...
}

struct Customer {
    pub id: Uuid,
    pub user_id: Uuid,
    pub name: String,
    // And so on...
}

我目前的目标是实现一个端点,它返回嵌套了客户的所有用户。例如:

// GET /users
// Response from endpoint
[{
  "id": "uuid-1",
  "email": "test@test.com",
  "customers": [{
    "id": "uuid-customer-1",
    "name": "Customer 1"
  }, {
    "id": "uuid-customer-2",
    "name": "Customer 2"
  }]
}]

上面的有效载荷可以使用以下结构来表示:

#[derive(Serialize)]
struct CustomerData {
    pub id: Uuid,
    pub name: String
}

#[derive(Serialize)]
struct UserData {
    pub id: Uuid,
    pub email: String,
    pub customers: Vec<CustomerData>
}

使用SQLx宏query_as!,我提出了以下解决方案尝试:

let result = sqlx::query_as!(
    UserData,
    r#"
    SELECT U.id, U.email, array_agg((C.id, C.name)) as "customers" FROM users U 
    INNER JOIN customers C ON user_id = U.id
    GROUP BY U.id
    "#
)
    .fetch_all(pool.as_ref())
    .await?;

但是,这会失败,因为array_agg返回的结果是RECORD[]类型,SQLx显然还不支持该类型。
这个问题让我想知道:

  • 有没有办法让SQLx正确地将array_agg的结果Map到customers
  • 假设我的实际模式在层次结构中至少有几个级别,那么这是正确的方法吗?将其拆分为几个查询并分段构造响应会“更好”吗?
x9ybnkn6

x9ybnkn61#

提前解释; TL;DR在底部。
经过更多的挖掘,我找到了一个解决方案,一旦你习惯了SQLx和Rust,这个解决方案实际上是非常明显的。
因此,问题是SQLx认为ARRAY_AGG()的返回值是RECORD[]类型。幸运的是,SQLX允许我们通过DSL中的类型转换来判断我们期望的类型。
因此,为了解决这个问题,我们首先需要在查询中将RECORD[]转换为Vec<CustomerData>,可以这样做:

SELECT 
        id, 
        email, 
        ARRAY_AGG((C.id, C.name)) as "customers: Vec<CustomerData>" 
    FROM users 
    JOIN customers C ON user_id = U.id
    GROUP BY id, email

此外,我们需要为CustomerData实现Trait sqlx::Type。幸运的是,有一个宏可以做到这一点:

#[derive(sqlx::Type, Serialize)]
struct CustomerData {
    // ...
}

最后但并非最不重要的是,还有一个问题需要解决:ARRAY_AGG返回NULL或数组。这可以通过三种方式来解决:

  • SQLx将可空类型视为Option<T>。因此,字段customers在Struct中的类型应为Option<Vec<CustomerData>>
  • 告诉SQLx不要担心,要高兴,并通过在查询转换中使用感叹号来Assert该值不是null。例如:... as "customers!: Vec<CustomerData>
  • ARRAY_AGG返回NULL时,使用SQL返回一个空数组(可以找到其他解决方案here):
SELECT 
        id, 
        email, 
        COALESCE(NULLIF(ARRAY_AGG((C.id, C.name)), '{NULL}'), '{}') as "customers: Vec<CustomerData>" 
    FROM users 
    JOIN customers C ON user_id = U.id
    GROUP BY id, email

如果层次结构深度不超过1,则此解决方案有效。如果嵌套得更深,则会因为SQLx的类型解析器中的bug而碰壁。关于这个问题可以在这里找到。
TL;DR:

  • 在嵌套类型上派生sqlx::Type
  • 使用以下**“as”**part使用SQLx的DSL转换查询结果
SELECT
  ARRAY_AGG(JOINED.id) as "field_name!: Vec<AggregateType>"
FROM ...

相关问题