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

Jul*_*sch 3 sql postgresql rust rust-sqlx

我正在开发一个用 Rust 编写的 REST API,使用 actix-web、SQLx 和 PostgreSQL 进行存储。假设这是我的模式(表示为 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...
}
Run Code Online (Sandbox Code Playgroud)

我当前的目标是实现一个端点,该端点返回所有用户及其嵌套的客户。即像这样:

// 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"
  }]
}]
Run Code Online (Sandbox Code Playgroud)

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

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

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

使用 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?;

Run Code Online (Sandbox Code Playgroud)

然而,这会失败,因为 返回的结果是array_agg类型RECORD[],而 SQLx 显然尚不支持该类型。

这个问题让我想知道:

  • 有没有办法让 SQLx 正确映射array_aggto的结果customers
  • 鉴于我的实际模式在层次结构中至少还有几个级别,这是否是正确的方法?将其拆分为多个查询并分段构建响应会“更好”吗?

Jul*_*sch 5

前面的解释;TL;DR 在底部。

经过更多的挖掘,我找到了一个解决方案,一旦你习惯了 SQLx 和 Rust,这实际上是非常明显的。

因此,问题是 SQLx 认为 的返回值是ARRAY_AGG()类型RECORD[]。幸运的是,SQLX 允许我们通过 DSL 中的类型转换来判断我们期望的类型。

因此,为了解决这个问题,我们首先需要在查询中将我们的RECORD[]to转换为: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
Run Code Online (Sandbox Code Playgroud)

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

#[derive(sqlx::Type, Serialize)]
struct CustomerData {
    // ...
}
Run Code Online (Sandbox Code Playgroud)

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

  • SQLx 将其视为可空类型Option<T>。所以该字段应该是Struct 中的customers类型。Option<Vec<CustomerData>>

  • 通过在查询转换中使用感叹号,告诉 SQLx 不要担心,要高兴并断言该值不为空。即像这样:... as "customers!: Vec<CustomerData>

  • 使用SQL返回一个空数组(其他解决方案可以在这里找到),当ARRAY_AGG返回NULL时:

    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
Run Code Online (Sandbox Code Playgroud)

如果层次结构深度不超过 1,则此解决方案有效。如果嵌套更深,您将因 SQLx 类型解析器中的错误而碰壁。与此相关的问题可以在这里找到。


长话短说:

  • 派生sqlx::Type嵌套类型。
  • 使用 SQLx 的 DSL 使用以下“as”部分转换查询结果
SELECT
  ARRAY_AGG(JOINED.id) as "field_name!: Vec<AggregateType>"
FROM ...
Run Code Online (Sandbox Code Playgroud)

编辑:令我沮丧的是,自定义枚举还算作层次结构中的一个级别。因此,如果您的嵌套类型具有枚举,则不支持这种情况。即使对于当前版本 0.7.1。