深度解析:ShardingSphere 二次开发全面支持 PostgreSQL 自定义 UDT 的实践与踩坑总结(超详细)

83 阅读6分钟

🚀 深度解析: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 → DescribeExecute

其中 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_enum
  • user_info
  • jsonb
  • age_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 阶段仅做:

  1. 注册 preparedStatement
  2. 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 等类型也有了技术底板。