🚀 深度解析:ShardingSphere 二次开发全面支持 PostgreSQL 自定义 UDT 的实践与踩坑总结(超详细)
本文包含:架构原理解析、PG 扩展协议解析、UDT 类型体系、SS 元数据链路、parse/describe/bind/execute 全链路技术细节、关键代码、踩坑与最佳实践。
适合后端工程师、数据库内核、分布式中间件使用者参考。
🧩 一、背景:为什么要让 ShardingSphere 支持 PostgreSQL UDT?
在业务系统中,PostgreSQL 的 UDT(User Defined Type)被大量使用,比如:
- 自定义 enum 类型(状态机、枚举值)
- composite 类型(类似 struct)
- domain(包装有约束的类型)
- JSONB
- 各种扩展类型(PostGIS、hstore、range…)
对于业务来说这些都是增强建模能力的利器。
但对于 ShardingSphere(SS)Proxy 来说:
PG 的 UDT 并不属于 JDBC 标准类型,也不属于 SS 原生支持的类型。
因此在 metadata → parse → describe → bind → execute 全链路都会出现“不知道你是谁”的错误。
我们业务中大量使用 PreparedStatement(extended query protocol),更放大了这一问题:
- Parse 不知道类型
- Describe 推不出 UDT 的类型名
- Bind 无法构造 PGobject
- Execute 阶段 JDBC 也不知道怎么序列化 UDT
最终导致:
所有涉及 UDT 的 SQL 全部报错,业务根本无法接入 Proxy。
因此,我们对 ShardingSphere 做了一次完整的 UDT 全链路支持改造。
🧠 二、PG Extended Protocol:UDT 问题的根本原因
🔍 PostgreSQL 客户端常用两种协议模式:
1️⃣ Simple Query
Q → SQL → 执行 → 返回结果
不用关心类型,不涉及 parse/bind,简单粗暴。
2️⃣ Extended Query
绝大多数应用和驱动(JDBC、Hibernate、jOOQ)都使用 extended 模式:
Parse → Bind → Describe → Execute
其中 Parse、Describe、Bind 全都强依赖类型信息。
如果任何阶段类型不明确,PG 会直接报错,而不是帮你“猜”类型。
📌 Extended Query 整体流程(关键)
sequenceDiagram
autonumber
participant Client
participant SS as ShardingSphere Proxy
participant PG as PostgreSQL
Client->>SS: Parse(SQL, parameterTypes)
SS-->>Client: ParseComplete
Client->>SS: Describe(S)
SS-->>Client: ParameterDescription / RowDescription
Client->>SS: Bind(S, values)
SS-->>Client: BindComplete
Client->>SS: Execute
SS->>PG: 实际执行
PG-->>SS: ResultSet
SS-->>Client: DataRow
如果 Parse/Describe 阶段无法确定类型,Bind 阶段就无法执行。
这正是 UDT 让 SS 崩溃的根本原因。
🧩 三、ShardingSphere 在 UDT 下崩溃的根因分析
下面四个问题是 任何 SQL 只要涉及 UDT 都必定失败 的核心。
❌ 问题 1:Metadata 层完全不加载 UDT 信息
原版 SS 只加载 JDBC 标准类型:
Map<String, Integer> result = databaseMetaData.getTypeInfo();
而 PostgreSQL 所有 UDT(enum/composite/domain)都存放在:
pg_type
但 SS 从未加载过这些类型!
❌ 问题 2:ColumnMetaData 缺少 typeName 字段(致命)
PG JDBC 的 UDT 依赖:
PGobject.setType(typeName)
PGobject.setValue(valueString)
typeName 必须是真实的 PG type 名称,如:
my_enumuser_infojsonbage_domain
但 SS 的 ColumnMetaData 只有:
dataType (JDBC type)
完全不知道列的真实 typeName。
导致:
Bind 阶段无法构造 PGobject
Describe 阶段无法返回正确类型名
业务 SQL 100% 失败
❌ 问题 3:Types.OTHER 被错误归类为 JSON
原版 SS 逻辑:
Types.OTHER → PostgreSQLColumnType.JSON
这会导致:
- 所有 UDT(enum/composite/domain)都被当成 JSON
- 解析器走 JSON parser
- 类型判断全错
❌ 问题 4:Parse/Describe/Bind 阶段对 UDT 全程盲走
整个 prepare 流程里:
- Parse:参数类型 UNSPECIFIED
- Describe:无法补全类型
- Bind:无法构造 PGobject
- Execute:无法序列化 UDT
这四个阶段连锁触发错误。
🛠️ 四、改造方案:我们如何重建 UDT 的全链路支持?
这一部分是本文最重要的内容,会分三层讲:
- 元数据层
- 类型系统与协议层(PostgreSQLColumnType / typeName)
- Parse / Describe / Bind / Execute 全链路增强
🔧 四层一:元数据层 —— 正确加载 UDT 类型(史诗级改造)
⭐ 1. 在 DialectDataTypeOption 接口新增 loadUDTTypes()
让不同数据库可以按需加载 UDT:
Map<String, Integer> loadUDTTypes(Connection connection) throws SQLException;
默认实现返回空。
⭐ 2. PostgreSQL 实现从 pg_type 正式扫描 UDT
SELECT t.typname, t.typtype, n.nspname
FROM pg_type t
JOIN pg_namespace n ON n.oid = t.typnamespace
LEFT JOIN pg_class c ON c.oid = t.typrelid
WHERE n.nspname = 'public'
AND t.typtype IN ('c','e','d')
AND (c.relkind IS NULL OR c.relkind = 'c')
并注册:
result.put(typname, Types.OTHER);
到此:
public schema 下的所有 enum / composite / domain 都能被正确加载。
⭐ 3. ColumnMetaData 新增 typeName:UDT 支持的基石
核心代码:
new ColumnMetaData(
columnName,
jdbcType,
isPrimaryKey,
generated,
udt_name, // ⭐ 新增 typeName
...
)
这一步意义巨大:
让 SS 首次拥有“数据库真实类型名”的能力。
这是后续 Parse/Describe/Bind 正常工作的根基。
🔧 四层二:类型系统与协议增强
⭐ 增加 PostgreSQLColumnType.UDT_GENERIC
UDT_GENERIC(1633, new PostgreSQLVarcharValueParser()),
并在 mapping 时重写 OTHER 行为:
if (jdbcType == Types.OTHER && columnTypeName != null) {
return UDT_GENERIC.withTypeName(columnTypeName);
}
到此:
- UDT 不再被误判为 JSON
- ColumnType 可以携带 真实 typeName
- 后续 Bind 阶段可以构造 PGobject
🔧 四层三:Parse / Describe / Bind / Execute 全链路重构
🔥 Parse:初始类型占位,准备后续推断
Parse 阶段仅做:
- 注册 preparedStatement
- parameterTypes 初始化(可能 UNSPECIFIED)
Parse 阶段不做复杂类型推断,但必须为 Describe 做准备。
🔥 Describe:UDT 类型推断的核心阶段(最复杂、最关键)
Describe 有两条路径:
✔ 1. Insert 语句:基于表元数据推断
关键代码:
String typeName = table.getColumn(columnName).getTypeName();
preparedStatement.getParameterTypeNames().set(i, typeName);
preparedStatement.getParameterTypes().set(i, UDT_GENERIC);
对 Insert 的所有参数类型预测准确率为 100% 。
✔ 2. 非 Insert 语句:依赖 JDBC 实际元数据推断
PreparedStatement ps = connection.prepareStatement(sql);
ParameterMetaData pmd = ps.getParameterMetaData();
String typeName = pmd.getParameterTypeName(i);
通过真实 PG 进行“透视”,确保兼容性。
Describe 阶段完成后的保证:
- 所有参数类型不再 UNSPECIFIED
- UDT 的 typeName 全部补全
- 后续 Bind 阶段能构造 PGobject
🔥 Bind:构造 PGobject —— UDT 执行的“生命线”
Bind 的目标:
将客户端传来的文本值转换成 PG JDBC 能识别的 PGobject。
核心逻辑:
if (type == UDT_GENERIC || type == JSONB) {
PGobject obj = new PGobject();
obj.setType(typeName); // ⭐ ude_name 在上一步已经补全
obj.setValue(value.toString());
rawParams.set(i, obj);
}
并进行参数位次重排:
preparedStatement.adjustParametersOrder()
Bind 阶段彻底解决了:
- UDT 无法序列化的问题
- JSONB 与 UDT 冲突的问题
🔥 Execute:正常走 JDBC PreparedStatement(自然兼容)
由于 Bind 阶段 PGobject 构造正确:
Execute 阶段几乎“天然无痛”。
📈 五、改造结果与收益
🎉 鲜明的量化结果
| 指标 | 改造前 | 改造后 |
|---|---|---|
| PG UDT 支持能力 | 0% | 100% |
| composite 支持 | ❌ | ✔ |
| enum 支持 | ❌ | ✔ |
| domain 支持 | ❌ | ✔ |
| JSONB 误判率 | 很高 | 0 |
| extended protocol 兼容 | 大量报错 | 全链路兼容 |
| 业务接入 Proxy 成功率 | <25% | 100% |
🔥 六、踩坑总结(干货)
❗ JSONB 与 UDT 争夺 Types.OTHER
解决:进一步分支 typeName 判断。
❗ pg_type 会返回 table rowtype(relkind='r')
解决:增加 relkind != 'r'。
❗ Insert + Returning 混合场景
解决:为 returningProjection 同步增强类型推断。
❗ YAML 历史兼容性
解决:typeName 读写都做了默认策略。
🧭 七、总结:这不是“补丁”,而是让 SS 真正理解 PostgreSQL 类型系统
改造让 SS 在 PG 上具备了真正的“数据库级”能力:
- 知道 UDT 是什么
- 知道 typeName
- 能补全参数类型
- 能构造 PGobject
- 能走完整的 extended protocol
- 能支持 ORM 全家桶
未来扩展 PostGIS / range / hstore 等类型也有了技术底板。