一、综述
目前的项目最初基于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的解决思路:
- 在定义EJB时,将blob类型改为byte[];
- 重载Hibernate中的PostgresDialect类的useInputStreamToInsertBlob()方法。
经实际测试,两种思路均不甚成功,可能是与版本差异和环境差异有关,还需要修改一些其它因素才行。未继续深入研究,部分原因是因为时间紧迫,部分原因是在解决TEXT时顺带解决了(见下)。
对于TEXT,资料3提出三种解决思路:
- 定义EJB时,取消@Lob标注,按String对待;
- 定义EJB时,保留@Lob标注,增加
@Type(type = "org.hibernate.type.TextType")标注;
- 不修改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
其中给出了很多种解决思路,现简单总结如下:
- 定义PostgreSql表结构时,将JSON改为TEXT,即仿照Oracle的做法;
- 扩展Hibernate中的Type,增加关于json的自定义类型,同时增加(或重载)处理JSON的相应方法;
- 更换JDBC驱动为pgjdbc-ng,它提供了可以处理JSON与TEXT转换的@Conveter标注;
- 在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,增加隐式或显式的类型转换方法。