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,增加隐式或显式的类型转换方法。
posted @ 2017-11-11 10:35  闻歌感旧  阅读(5890)  评论(0编辑  收藏  举报