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) | VarChar | String |
password text | Text | String |
即使两个字段都是字符串,也不能交换顺序。解码按位置读取,不会根据 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 验证。
备注:内容仅供参考。
