• JPA/Hibernate移植到PostgreSQL时关于CLOB, BLOB及JSON类型的处理


    一、综述

    目前的项目最初基于Oracle开发,现在要移植到PostgreSQL。鉴于已经使用JPA/Hibernate来实现对象的持久化,领导总以为迁移任务很easy,但实际过程中还是出现了很多问题。

    这其中有一些问题是定义EJB时不规范引起的,如把Number(1)映射为boolean、Number(n)映射为String、Date映射为String等等。因为Oracle拥有强大的自动类型转换能力,只要数据符合格式,Oracle不会报错;一旦移植到PostgreSQL环境,各种类型不匹配的Exception抛来抛去。不过只要按规范一一修正过来,这些问题还是容易解决的。

    另外几个问题就很令人头疼,特别是关于CLOB, BLOB及JSON类型的处理。移植到PostgreSQL时,绝不是简单地将CLOB替换为TEXT、BLOB替换为BYTEA、Varchar2(...) CONSTRAINT ... CHECK (... IS JSON) 替换为JSON后,余下的交给JPA就能搞定。接下来的麻烦得自己去一一去解决。

    本文即是根据搜索到的资料,加上自己操作过程中的经验,进行一些实践上的总结。

    二、CLOB和BLOB的处理

    1 现象

    对于CLOB(PostgreSQL对应的是TEXT,后文不作区分)类型,写入时不抛例外,但实际上存储的是一个数值,而不是byte[]内容;读取时,部分行正确,部分行抛例外:column xxx is of type text but expression is of type bigint ...;对于BLOB(对应的是BYTEA,不作区分)类型,干脆写入时就报错:column xxx is type of bytea but expression is type of bigint ...。

    2 PostgreSQL处理LOB数据的两种方式

    要解释原因,首先需知道PostgreSQL处理LOB数据的两种方式:oid + largeobject 和byte[],详细说明参阅:

    参考资料1:https://jdbc.postgresql.org/documentation/80/binary-data.html

    很明显,JPA把期望的二进制数组方式当作oid+largeobject方式传递给了PostgreSQL,于是当遇到写入CLOB或BLOB时,相应字段存入的实际上是oid的值(BigInt类型),而byte[]的值则被写入到公共的pg_largeobject表。区别在于,BigInt类型的oid自动转换到TEXT时成功了,转换到BYTEA时失败。

    为验证这一说法,用已经存入TEXT字段的数值去pg_largeobject查询,确实是期望的byte[]的值,这也是JPA读取时有些能成功的原因;至于不成功的那些记录,猜测可能与字节数有关,因为字节数超过1M的都成功而在K级别的都失败(临界值未知)。鉴于篇幅,这些内容不展开,有兴趣者请自行验证。

    oid + largeobject 方式除了性能上有些优势外,至少有三个缺点:1 公用的pg_largeobject存在权限问题;2 pg_largeobject的相应记录不会随源记录删除而自动删除;3 对事务有较严格限制。因此并不符合项目要求,但为什么JPA总是按oid + largeobject方式来处理?

    3 Hibernate与PostgreSQL的不统一之处

    以BYTEA为例,PostgreSQL的两种处理方式是通过分别调用JDBC的setBinaryStream()和setBlob()接口来实现的。期望的逻辑应该是Hibernate能针对PostgreSQL的这个特点来正确区分、正确调用,但不幸的是:Hibernate以为所有数据库都是调用setBinaryStream()来写入BYTEA,出于某种原因并不打算照顾PostgreSQL的特殊情况(貌似一段时间内不会改观),于是前面提到的错误现象发生了。

    详细的解释请参阅:

    参考资料2:http://www.codeweblog.com/postgresql-hibernate-on-bytea-and-oid-mapping-problem/

     至于TEXT,情况大致类似,只是调用的是另外两个JDBC接口,不再展开。

    4 解决办法

    还是在参考资料2,提出两种解决针对BYTEA的解决思路:

    1. 在定义EJB时,将blob类型改为byte[];
    2. 重载Hibernate中的PostgresDialect类的useInputStreamToInsertBlob()方法。

    经实际测试,两种思路均不甚成功,可能是与版本差异和环境差异有关,还需要修改一些其它因素才行。未继续深入研究,部分原因是因为时间紧迫,部分原因是在解决TEXT时顺带解决了(见下)。

    对于TEXT,资料3提出三种解决思路:

    参考资料3:https://stackoverflow.com/questions/28588311/correct-jpa-annotation-for-postgresqls-text-type-without-hibernate-annotations

    1. 定义EJB时,取消@Lob标注,按String对待;
    2. 定义EJB时,保留@Lob标注,增加 @Type(type = "org.hibernate.type.TextType")标注;
    3. 不修改EJB,重载PostgresDialect类remapSqlTypeDescriptor()方法,将CLOB当longvarchar处理。

    经实际测试,三种思路均可达到目的。由于上级领导不赞成修改标注的方式(理由是与Oracle环境的版本不一致),遂采用思路3,顺带着将BLOB按longVarBinary处理。

    因项目中PostgreSQL是9.4版,故选择从PostgreSQL94Dialect继承,一般情况下可选择PostgreSQL9Dialect。代码为:

    package com.xxx.pgdialect;
    
    import java.sql.Types;
    
    import org.hibernate.dialect.PostgreSQL94Dialect;
    import org.hibernate.type.descriptor.sql.LongVarbinaryTypeDescriptor;
    import org.hibernate.type.descriptor.sql.LongVarcharTypeDescriptor;
    import org.hibernate.type.descriptor.sql.SqlTypeDescriptor;
    
    public class PgDialect extends PostgreSQL94Dialect
    {
        @Override
        public SqlTypeDescriptor remapSqlTypeDescriptor(SqlTypeDescriptor sqlTypeDescriptor)
        {
            switch (sqlTypeDescriptor.getSqlType())
            {
            case Types.CLOB:
                return LongVarcharTypeDescriptor.INSTANCE;
            case Types.BLOB:
                return LongVarbinaryTypeDescriptor.INSTANCE;
            }
            return super.remapSqlTypeDescriptor(sqlTypeDescriptor);
        }
    }

    然后在persistence.xml中用这个类(com.xxx.pgdialect.PgDialect)替换PostgreSQL94Dialect即可。

     三、JSON的处理

     JSON类型的特殊之处在于:首先Oracle实际上是按特殊的varchar2或clob来对待;其次是Hibernate及JDBC都没有定义json类型。因此,在遇到“column xxx is type of json but expression is type of character varying ...”例外时,不能简单地照搬前述方法。

    经Google,发现一篇很有价值的资料:

    参考资料4:https://stackoverflow.com/questions/15974474/mapping-postgresql-json-column-to-hibernate-value-type

    其中给出了很多种解决思路,现简单总结如下:

    1. 定义PostgreSql表结构时,将JSON改为TEXT,即仿照Oracle的做法;
    2. 扩展Hibernate中的Type,增加关于json的自定义类型,同时增加(或重载)处理JSON的相应方法;
    3. 更换JDBC驱动为pgjdbc-ng,它提供了可以处理JSON与TEXT转换的@Conveter标注;
    4. 在PostgreSql数据库,创建隐式或显式的类型转换方法或函数,使得PostgreSQL接受JSON与TEXT的自动转换。

    思路1需要应用程序保证数据符合json规范,风险较大,被否决。思路2有很多种具体实现方式(有兴趣者自行钻研),但其共同点都是需要修改EJB标注,被领导否决。思路3过于依赖某一产品,且跟2一样也要修改标注,也被否决。只剩下思路4,而事实上它也确实是最简便的方式。

    在psql命令行,简单创建TEXT与JSON、Varchar与JSON互相转换的四个CAST即可:

    CREATE CAST (text AS json)
      WITH INOUT
      AS ASSIGNMENT;
    
    CREATE CAST (json AS text)
      WITH INOUT
      AS ASSIGNMENT;
    
    CREATE CAST (varchar AS json)
      WITH INOUT
      AS ASSIGNMENT;
    
    CREATE CAST (json AS varchar)
      WITH INOUT
      AS ASSIGNMENT;

    执行之后,再无“column xxx is type of json but expression is type of character varying ...”例外。

    进一步猜测,XML类型也可以按类似方法来处理。

    四、总结

    • 定义EJB时一定要规范,可以避免大多数简单的类型不匹配错误;
    • 对于CLOB和BLOB,把它们按LongVarchar和LongVarBinary处理;
    • 对于JSON,增加隐式或显式的类型转换方法。
  • 相关阅读:
    jquery 读取file 图片文件的宽高
    log4net配置要点
    js 公用插件,教科书级的写法
    mvc4 发布,遇到 403.14 问题,并且iis提示打开目录浏览。。。解决办法
    js 处理 html 标签转义 处理json中含有的ascii 编码
    sqlserver ,left join 不仅可以join表,还可以是一个结果集
    .net MVC3 页面和 action 传值问题
    java web工程 数据库操作报驱动类找不到的错误
    MySQL快速构造百万数据
    python之eval函数的应用实例
  • 原文地址:https://www.cnblogs.com/wggj/p/7809832.html
Copyright © 2020-2023  润新知