PAYATHON 2026

Diesel 如何为 PostgreSQL 复合类型实现 ToSql 和 FromSql?

数据小陈

可以按字段顺序将复合类型映射为 Rust 元组:写入时调用 WriteTuple,读取时复用元组的 FromSql<Record<...>, Pg> 实现,再组装成 User。命名复合类型仍使用你定义的 schema::sql_types::User 作为 SQL 类型。WriteTuple 文档、Record 文档

实现 ToSql 和 FromSql

原问题未说明 Diesel 版本。下面使用 Diesel 2.x 的 API 写法,项目需要启用 postgres feature。

保留现有的 SQL 类型声明和 User 模型,在 User 所在模块中添加以下实现。如果 schema 不在 crate 根目录,需要调整导入路径。

use diesel::deserialize::{self, FromSql};
use diesel::pg::{Pg, PgValue};
use diesel::serialize::{self, Output, ToSql, WriteTuple};
use diesel::sql_types::{Record, Text, VarChar};

use crate::schema::sql_types::User as UserSql;

impl ToSql<UserSql, Pg> for User {
    fn to_sql<'b>(
        &'b self,
        out: &mut Output<'b, '_, Pg>,
    ) -> serialize::Result {
        WriteTuple::<(VarChar, Text)>::write_tuple(
            &(self.username.as_str(), self.password.as_str()),
            &mut out.reborrow(),
        )
    }
}

impl FromSql<UserSql, Pg> for User {
    fn from_sql(value: PgValue<'_>) -> deserialize::Result<Self> {
        let (username, password) =
            <(String, String) as FromSql<
                Record<(VarChar, Text)>,
                Pg,
            >>::from_sql(value)?;

        Ok(Self { username, password })
    }
}

这里将 SQL 类型标记导入为 UserSql,与保存实际数据的 Rust 结构体 User 区分。

为什么需要 WriteTuple

PostgreSQL 复合类型的二进制数据包含字段数量,以及各字段的类型信息、长度和内容。直接拼接两个字符串再写入,无法得到正确的复合类型编码。

PostgreSQL 对匿名 record 的绑定参数有限制,不能直接将普通元组作为命名复合类型参数。Diesel 为命名复合类型提供的 WriteTuple 负责生成所需编码;读取时,由 Record 对应的元组实现拆解字段。Diesel 复合类型实现

字段映射必须遵循 CREATE TYPE 中的声明顺序:

PostgreSQL 字段Diesel SQL 类型Rust 类型
username varchar(20)VarCharString
password textTextString

即使两个字段都是字符串,也不能交换顺序。解码按位置读取,不会根据 Rust 字段名查找数据库属性。

如何用于 user[] 数组

完成单个 User 的读写实现后,可以复用 Diesel 的数组支持,无需另写数组编码逻辑。Diesel 官方的自定义数组示例也采用这种方式。自定义类型数组示例

需要核对 schema 中数组元素的可空性:

  • Array<UserSql> 对应 Vec<User>,要求读取的数据不含 SQL NULL 元素。
  • Array<Nullable<UserSql>> 对应 Vec<Option<User>>,可以表示 SQL NULL 元素。

users "user"[] NOT NULL 只限制整个数组不能为 NULL,不禁止数组内部出现 NULL 元素。因此,如果生成的 schema 使用 Array<Nullable<UserSql>>,模型也应使用 Vec<Option<User>>。

字段为 NULL 时怎么处理

当前实现将复合类型内部的两个字段都读成 String。如果任一字段实际为 NULL,反序列化就会失败。

如果业务允许 username 为 NULL,需要同时修改模型和元组映射:

// Rust 字段
pub username: Option<String>,

// SQL 字段元组
Record<(Nullable<VarChar>, Text)>

// Rust 字段元组
(Option<String>, String)

写入时也要使用 WriteTuple::<(Nullable<VarChar>, Text)>,并传入 self.username.as_deref()。复合类型内部字段是否为 NULL,与数组元素是否为 NULL 是两层独立的配置。

最后,在实际数据库中分别检查单个复合值、多个元素的数组和空数组的写入与读取结果;如果允许 NULL,还需补充对应场景。上述代码未在题目项目中运行,仍需结合实际 Diesel 版本和 schema 验证。

备注:内容仅供参考。