针对RDBMS的统一类型处理与写入模型——UTPW

背景

在数据集成领域,对于数据的读取和写入是其中主要需要解决的问题。针对异构数据源设计一种统一的类型处理与写入范式尤为重要。本文主要提出一种统一类型处理与写入模型 UTPW(Unified Type Process and Write),使得利用实现 JDBC 标准的驱动将数据写入数据库时候能够更加灵活、高效。

UTPW 实现目标

  • 类型处理透明:用户基于 JDBC API 写入数据时,无需关心数据类型,模型自动处理。应用 UTPW 后用户只要熟悉基本 PreparedStatement 的使用即可透明化处理类型
  • 针对各种数据库的 JDBC 驱动实现具有较高的兼容性:不同厂商的 JDBC 驱动实现上有一些差异,对于 JDBC 标准的遵循度也不相同。例如 Oracle、SqlServer 对于 java.sql.Types 都是不兼容的
  • 准确、完整、智能的类型处理:统一模型能够处理对应数据库的完整类型,例如 PostgreSQL 的地理信息类型等。在遇到非法格式的写入时, 也能提前定位数据异常,给出精准的错误提示。

整体模型设计

关于 schema service 补充说明

UTPW 只聚焦于统一类型处理与写入,对于列元信息的获取,一般通过元信息服务可以获取。理论上 UTPW 中的统一列类型也可以包含在 schema service 的实现中,不过这边我们不展开,只聚焦于 UTPW 本身。架构中的 schema service 可以理解为一个已经实现好的元数据服务,用于我们获取必要的列元信息。

UTPW 架构

Strict type name 和 General type name

  • strict type name: 从数据库元信息表中获取的 data type 信息,内容严格匹配
  • general type name: UTPW 模型定义的广义类型名称,模型要求实现上尽量与 strict type name 保持一致,除非是某些动态类型无法保持一致,这个下面讲 Unified Field Type 核心三元素的时候会提到。

Unified Field Type 核心三元素定义

架构图上最重要的就是对 Unified Field 中三个基本元素的定义。

  • **General Type Code: **在 java.sql.Types 中 JDBC 标准已经给出了一些 type code 的定义,但是不同数据库由自己不同的实现。这里之所以称为 general type code 意味着这个 type code 是广义的,可以包含数据库厂商自己实现的。由于 JDBC 标准 PreparedStatement 的入参制约,这个 code 肯定是这个整型,所以我们此处是可以统一定义的。在执行 PreparedStatement.setObject 的时候,我们可以使用这个 code 让驱动识别这个类型。即使是数据库厂商自己实现的类型,厂商的驱动肯定是可以自己识别的,例如 OracleTypes 还有 PG 的 type oid。
  • General Type Name: 这个是关联列元信息和 Unified Field Type 的桥梁。这个 general type name 默认情况下,模型要求其 typeName 和数据库元信息表中的 data type 保持严格一致。但是有些数据库 data type 会关联列的 precision 和 scale 导致具备动态性,这种情况下 Unified Field Type 只要提供一个 findByStrictDbTypeName 方法即可,这个在 UTPW 模型中也是有定义该顶层接口。下图是 Oracle 动态类型的一个示例

  • Type Handler: 这个显然就是具体的类型处理了。具体的 TypeHandler 实现由 UTPW 模型负责,这个需要了解具体数据库驱动的实现,才可以有针对性的实现。例如 Oracle 驱动支持 setObject 直接写入一些 OracleTypes,而 PG 则支持 PGObject 直接通过 PreparedStatement 进行 set。这个在下文中涉及具体 driver JDBC 实现的时候我们会再提到,加深感知。

UTPW 模型规范

  • **Unified field type 必须包含核心三要素: **必须包含核心三要素,才可以完成 UTPW 的核心能力
  • general type name 必须可以和数据库元信息 data type 一一关联映射: 这样才可以确保应用可以根据列元信息获取到 unified field type
  • 优先使用驱动层面的 type 定义:UTPW 不要求驱动实现的类型定义遵循 java.sql.Types。驱动实现层面如果有完整 type 定义,Unified Field Type 优先使用驱动层面 type 定义。确认驱动 type 定义符合 JDBC 类型标准,才可以考虑使用 java.sql.Types 来定义 Unified Field Type
  • 数据源实现分离:由于依赖驱动,为了避免作为三方包被使用时依赖太多驱动包,各个数据源的 UTPW 模型实现需要分离,可以被单独引用
  • 驱动需要支持 JDBC API 支持使用 setObject 统一设置:使用 UTPW 的类型时,使用 setObject 统一写入。支持 JDBC API 意味着 setObject 可以使用 SQLType 或者 int 的 type code

  • 优先使用 SQLType 的 setObject: 像 Oracle、MySQL 都有自己实现的内置 Type 类型,驱动实现自动转换也支持的更好。如果使用 int type 进行 setObject,则有些类型不太好处理。例如 mysql 如果不使用 MysqlType,那么 YEAR 类型会使用 DATE 的 type code,这就不太好处理。

核心设计思想

  • 类型处理驱动委托:驱动层面能够利用的类型处理会尽量利用。这个理念的关键在于 type handler 的实现上会充分利用数据库驱动的处理。例如 Oracle 的 OracleTypes、PostgreSQL 的 PGObject,这个下文我们介绍驱动实现时会给出一些例子。这种委托方式主要有以下好处:
    • 驱动版本无关:只要驱动顶层的对象 API 以及 JDBC API 不变化,具体类型处理细节的变化对 UTPW 是透明的
    • 更加准确:自己也能去做一些类型处理,但是一般而言肯定没有原厂的实现更加准确
  • 统一 JDBC 标准写入:UTPW 模型要求统一使用 PreparedStatement.setObject(Object val,int typeCode)的 JDBC API 写入。这样的主要好处是:
    • 简单:用户只要将任意一个 Object 扔给 UTPW 模型即可自动化处理
    • 兼容性好:不要求驱动对 JDBC API 有非常完整和深度的兼容,只要驱动实现 PreparedStatement.setObject 方法即可应用 UTPW

UTPW 模型使用限制

驱动需要支持 JDBC 标准中 PreparedStatement 的写入方式

UTPW 模型仅仅适用于实现 JDBC 标准的驱动(支持基于 PreparedStatement.setObject 方式的写入,指定 object 和 type code)。像 ElasticSearch、Kafka 以及一些新型的 OLAP 数据库写入方式没有统一标准,也不符合 JDBC API 标准,不适用该模型。UTPW 不要求数据库驱动对 JDBC 标准有非常好的兼容,只需要支持 PreparedStatement.setObject(Object val,int typeCode)这种写入方式即可

TIPS: 怎样的数据库驱动/Client 符合 JDBC 标准?
这个在Java Doc中有说明。简单来说,就是驱动或者客户端实现了 JDBC API,java 应用程序可以通过实现 JDBC API 的驱动访问数据库则可以认为这个数据库驱动符合 JDBC 标准。不过具体有多么符合,各个厂家的标准遵循度存在一些差异。JDBC API 的接口定义主要存放在 java.sql 和 javax.sql,其中最主要的类是 java.sql.Types 和 java.sql.PreparedStatement

驱动实现需向下兼容

UTPW 模型落地依赖驱动的实现,包括其中定义的一些类型对象。如果驱动不向下兼容,可能导致一些类型编译报错。一般 UTPW 模型针对向下兼容的驱动实现,建议使用最新的版本

主流 RDB 驱动实现

这节会介绍下主流的 RBD 驱动实现并且结合其驱动实现来理解 UTPW 的设计

MySQL

源码解读

  • 源码版本:8.0.22
  • 核心类:
    • ClientPreparedStatement.java
    • MysqlType

找到 JDBC API 的实现类 ClientPreparedStatement,通过下钻查看其 setObject 方法的实现在 AbstractQueryBindings 中,类部分内容截图如下图所示。查看源码我们可以得到关键信息为:

  • ** MySQL 驱动识别的类型为 java.sql.Types 类型和自定义类型 MysqlTypes**:mysql 类型系统相比别的数据库不是那么复杂,驱动层面没有内置的一些复杂类型对象,在 setObject 实现中采用基本两个步骤完成数据的转换:
    1. 识别 value 的类型
    2. 识别 mysql type
  • 对 String 的 value 有很好的兼容性:当判断值是 String 类型时,第二步会识别 mysql type 然后针对性地做转换,主要是针对时间会进行自动化的处理
  • 驱动自定义 type code 完全映射 jdbc type code:: com.mysql.cj.MysqlType 和 java.sql.Types 一一映射。定义 unified field type 可以直接使用 MysqlType。setObject 的时候对于使用 java.sql.Types 的 code 或者 MysqlType 的 code 效果是一样的
  • Mysql 不存在复杂对象的内置类型对象:mysql 类型系统比较简单,驱动层面没有专门针对复杂类型定义内置类型对象
  • setObject 接收 java.sql.Types 的 code,实现了 JDBC API:接收的 type code 会转化成 MysqlType,然后继续往下调用

针对 String 类型的输入时,时间会自动处理,这里默认会取 local timezone 和 session timezone 做时区处理,请留意

Unified Type 实现

由于 MySQL 对 String 类型的写入支持比较好,大部分类型的处理我们用一个 StringTypeHandler 即可统一处理

Oracle

源码解读

  • 源码版本:ojdbc8-19.8.0.0.jar
  • 核心类:
    • oracle.jdbc.driver.OraclePreparedStatement
    • oracle.jdbc.OracleTypes

同样的,我们先看 setObject 的入口 OraclePreparedStatement,总体上类型处理流程和 mysql 不太相同,总体步骤为:

  1. 先判断 type code
  2. 判断 val 类型进行转换

源码中需要理解的关键点:

  • 复杂类型优先使用 Oracle 内置类型,例如 INTERVALDS:虽然 oracle 实现提供了些复杂类型的 convert,但是基本上都是针对内置类型几个构造函数重载去转换的。这种隐试转换是否兼容不太可控,因此在实现 type handler 的时候,优先使用驱动内置对象
  • Oracle 支持内置复杂类型对象:oracle 驱动层面内置一些复杂类型对象,并且提供一些基本的 convert 能力,利用驱动的能力我们可以利用这些复杂对象来实现 type handler
  • setObject 类型处理优先严格使用内置复杂对象:复杂对象通过驱动写入,必须使用内置复杂对象
  • setObject 强依赖驱动自定义 type code 实现:驱动层面的 type code 定义是和 java.sql.Types 是兼容的,对于 oracle 来说,java.sql.Types 是 oracle.jdbc.OracleTypes 的子集。setObject 时直接识别的是驱动自定义的 type code。因此定义 Unified Field Type 的时候需要引用驱动内的 type 来定义,由于不存在驱动层面的映射,如果直接使用 java.sql.Types 会导致某些类型无法正常写入
  • setObject 接收 Oracle 驱动定义的 type code,顶层接口实现了 JDBC API: 这个要求我们定义 unified field type 需要使用 oracle 驱动的 code,否则一些类型会无法写入


下图是 BINARY_DOUBLE 内置类型,只接收特定集中类型的输入,驱动实现的 convert 也只兼容如下几种类型

下图是 oracle 内置的复杂对象类

下图是必须驱动 setObject 必须使用内置复杂对象的案例:

Unified Type 实现

  • 利用内置对象定义复杂对象 handler: Oracle 有比较多的复杂类型,在 Unified Type 实现上,主要是利用其内置的对象来对输入的 object 值进行处理,然后 setObject 的时候可以直接使用
  • Unified Field Type 定义需使用内置 type: 驱动 setObject 使用的是驱动自定义的类型 code,因此定义 unified field type 也需要使用 OracleType 中的 type code

PostgreSQL

源码解读

  • 源码版本:postgresql-42.2.20.jar
  • 核心类:
    • org.postgresql.jdbc.PgPreparedStatement
    • org.postgresql.core.Oid
    • org.postgresql.util.PGObject

同样的,我们先看 setObject 的入口 PgPreparedStatement,总体上类型处理流程为:

  1. 先判断 type code
  2. 判断 val 类型进行转换

阅读源码需要了解的核心信息:

  • PG 的内置 type code 都定义在 Oid:Oid 不是 java sql Types 的实现类,是 PG 独立定义的
  • setObject 可完全依赖 java.sql.Types:虽然驱动层面的 oid type code 和 java.sql.Types 是不兼容的,单是 Oid 用来识别 pg 的列类型,仅仅配合 PGObject 在驱动层内部实现使用。setObject 方法是完全可以识别 java.sql.Types。对于一些由 Oid 内部 type code 关联的复杂类型,通过 Type.OTHER 即可映射。因此 UTPW field type 定义时只要使用 java.sql.Types 即可。
  • setObject 接收 java.sql.Types 中的 type code,实现了 JDBC API: 驱动实现根据 PGObject 会自动转换关联内置 Oid type code
  • 支持内置对象以及内置对象 setObject 写入:PG 的内置对象都是实现 PGObject 的。setObject 支持 PGObject 写入以及驱动自动处理。驱动内部处理和识别 PGObject 的时候会使用 Oid type code。我们可以向 Oracle 一样实现针对内置对象的 type handler 来处理复杂类型。

Unified Type 实现

了解了 PG 的 type code 以及 setObject 的原理后,实现 PG 的 UTPW 模型就容易了。

  • 常见类型基本用 StringTypeHandler 和 BinaryTypeHandler 即可,复杂对象可以像 Oracle 一样实现对应的 type handler 即可
  • 由于 setObject 都是接收的 java.sql.Types,type 定义可以使用 java.sql.Types 来定义。Oid 只是驱动内部实现识别 PgObject 的时候会自动使用而已

SqlServer

源码解读

  • 源码版本:mssql-jdbc-9.4.0.jre8.jar
  • 核心类:
    • com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement
    • com.microsoft.sqlserver.jdbc.JavaType: SqlServer 会自动识别输入的 object 类型属于什么 java type
    • com.microsoft.sqlserver.jdbc.JDBCType: 这个是用于真实写入数据库使用的 jdbc type

同样的,我们先看 setObject 的入口 PgPreparedStatement。SqlServer 的对象处理相比别的驱动实现定义了更多的一些类,复杂度高一些。不过总体上类型处理流程还是可以清晰的看到的

  1. 接收 java type 和 jdbc type
  2. 针对各种类型做一些提前判断、校验和转换
  3. 最终写入利用 DTV 对象的实现类,执行 executeOp 方法

源码层面主要还是理解 setObject 的逻辑以及内置 type 的处理,总结如下:

  • 自定义 JDBCType 和 JavaType: sqlserver 驱动自定义了两种 type,作为 setObject 中的重要入参。javaType 定义了输入的 Object 归属的 java 类型,JDBCType 定义了数据库层面的 type 类型
  • setObject 方法依赖 com.microsoft.sqlserver.jdbc.JDBCType 中的 code:驱动层面 com.microsoft.sqlserver.jdbc.JDBCType 的 type code 兼容了 java.sql.Types。并且 setObject 驱动实现的时候也支持 java.sql.Types,但是如果仅仅使用 java.sql.Types 会导致某些类型在 setObject 中没法处理,因此定义 Unified Field Type 的时候需要直接使用 com.microsoft.sqlserver.jdbc.JDBCType 中的 int code
  • setObject 方法实现 JDBC API 接收 java.sql.Types 中的 TypeCode: 实现逻辑和 PG 很像,内部回去识别输入对象的类型并且进行转换
  • 支持内置类型:主要是地理相关的类型,参考 SQLServerSpatialDatatype 的实现类,像 Array 这种,SqlServer 是不支持的。类型支持上 pg 还是比较强的

Unified Type 实现

了解 SqlServer 驱动层面的类型定义之后就清楚了,在定义 UnifiedFieldType 的时候可以使用 SqlServer 层面定义好的 JDBCType 当中的 type code 即可,这个 type code 是包含 java.sql.Type 的

setObject 依赖的 type code 总结

我们实现 unified field type 的时候,需要根据驱动层面 setObject 对 type code 的使用方式从而确定如何来自定义 unified field type。针对不同数据库定义 unified field type 依赖的类,建议使用 setObject 关联的 type code 来源类,总结如下

数据库 setObject 关联的 type code 来源类 setObject 是否实现 JDBC API
MySql com.mysql.cj.MysqlType
Oracle oracle.jdbc.OracleType
PostgreSQL java.sql.Types
SqlServer com.microsoft.sqlserver.jdbc.JDBCType

总结

本位介绍了 UTPW 模型的核心设计理念和实现思路。不过具体落地实现以下工作量仍然是不可避免的:

  • 查看 driver 实现,确认其包含哪些内置对象,这个用于定义处理复杂对象的 type handler
  • 查看 driver 实现,确认其 setObject 关联的 type code 来源类是什么,这个用于定义 unified field type
  • 查看 driver 实现,确认其 setObject 关联的 setObject 是否实现 JDBC API
  • 查看 driver 实现,确认其针对 String、binary 类型的输入时,默认支持的隐试转换规格是怎样的,这样可以针对大部分简单类型使用通用的 type handler