標籤:get 原因 post 問題 extends java rac 針對 sig
一、綜述
目前的項目最初基於Oracle開發,現在要移植到PostgreSQL。鑒於已經使用JPA來實現對象的持久化,領導總以為遷移任務很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提出三種解決思路:
參考資料3:https://stackoverflow.com/questions/28588311/correct-jpa-annotation-for-postgresqls-text-type-without-hibernate-annotations
- 定義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,增加隱式或顯式的類型轉換方法。
JPA移植到PostgreSQL時關於CLOB, BLOB及JSON類型的處理