针对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架构

image.png

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动态类型的一个示例

image.png

  • 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

image.png

  • 优先使用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,然后继续往下调用

image.png

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

Unified Type实现

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

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,否则一些类型会无法写入

image.png

Unified Type实现

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

image.png

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来处理复杂类型。

image.png

image.png

Unified Type实现

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

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

image.png

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方法

image.png

image.png

源码层面主要还是理解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还是比较强的

image.png

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