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,增加隱式或顯式的類型轉換方法。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM