2013-04-12 183 views
61

我在postgreSQL數據庫(9.2)中有一個包含json類型列的表。我很難將此列映射到JPA2實體字段類型。將postgreSQL JSON列映射到Hibernate值類型

我試圖使用字符串,但是當我保存實體時,我得到一個異常,它無法將字符轉換爲json。

處理JSON列時使用的正確值類型是什麼?

@Entity 
public class MyEntity { 

    private String jsonPayload; // this maps to a json column 

    public MyEntity() { 
    } 
} 

一個簡單的解決方法是定義一個文本列。

+1

我知道這是一個有點舊的,但看看我的答案http://stackoverflow.com/a/26126168/1535995類似的問題 – Sasa7812

回答

34

See PgJDBC bug #265

PostgreSQL對數據類型轉換過分嚴格。它不會隱含地將text轉換爲文本類型的值,例如xmljson

解決此問題的嚴格正確方法是編寫一個使用JDBC setObject方法的自定義Hibernate映射類型。這可能有點麻煩,所以你可能只想通過創建一個較弱的演員來減少PostgreSQL的嚴格性。

正如@markdsievers在評論和this blog post中指出的,此答案中的原始解決方案繞過了JSON驗證。所以這不是你想要的。它的安全寫:

CREATE OR REPLACE FUNCTION json_intext(text) RETURNS json AS $$ 
SELECT json_in($1::cstring); 
$$ LANGUAGE SQL IMMUTABLE; 

CREATE CAST (text AS json) WITH FUNCTION json_intext(text) AS IMPLICIT; 

AS IMPLICIT告訴PostgreSQL的它可以在不被明確告知,讓這樣的事情工作轉換:

regress=# CREATE TABLE jsontext(x json); 
CREATE TABLE 
regress=# PREPARE test(text) AS INSERT INTO jsontext(x) VALUES ($1); 
PREPARE 
regress=# EXECUTE test('{}') 
INSERT 0 1 

感謝@markdsievers您指出的問題。

+0

感謝您的解決方法! –

+2

值得一讀這個答案的結果[博客文章](http://www.pateldenish.com/2013/05/inserting-json-data-into-postgres-using-jdbc-driver.html)。特別是註釋部分強調了這種危險(允許無效的json)和替代/優越的解決方案。 – markdsievers

+0

@markdsievers Thankyou。我已經用更正後的解決方案更新了這篇文章。 –

67

如果你有興趣,這裏有一些代碼片段來獲取Hibernate自定義用戶類型。首先擴展PostgreSQL的方言告訴它有關JSON類型,感謝Craig林格的JAVA_OBJECT指針:

import org.hibernate.dialect.PostgreSQL9Dialect; 

import java.sql.Types; 

/** 
* Wrap default PostgreSQL9Dialect with 'json' type. 
* 
* @author timfulmer 
*/ 
public class JsonPostgreSQLDialect extends PostgreSQL9Dialect { 

    public JsonPostgreSQLDialect() { 

     super(); 

     this.registerColumnType(Types.JAVA_OBJECT, "json"); 
    } 
} 

下實現org.hibernate.usertype.UserType。下面的實現將String值映射到json數據庫類型,反之亦然。記住字符串在Java中是不可變的。可以使用更復雜的實現將自定義Java Bean映射到存儲在數據庫中的JSON。

package foo; 

import org.hibernate.HibernateException; 
import org.hibernate.engine.spi.SessionImplementor; 
import org.hibernate.usertype.UserType; 

import java.io.Serializable; 
import java.sql.PreparedStatement; 
import java.sql.ResultSet; 
import java.sql.SQLException; 
import java.sql.Types; 

/** 
* @author timfulmer 
*/ 
public class StringJsonUserType implements UserType { 

    /** 
    * Return the SQL type codes for the columns mapped by this type. The 
    * codes are defined on <tt>java.sql.Types</tt>. 
    * 
    * @return int[] the typecodes 
    * @see java.sql.Types 
    */ 
    @Override 
    public int[] sqlTypes() { 
     return new int[] { Types.JAVA_OBJECT}; 
    } 

    /** 
    * The class returned by <tt>nullSafeGet()</tt>. 
    * 
    * @return Class 
    */ 
    @Override 
    public Class returnedClass() { 
     return String.class; 
    } 

    /** 
    * Compare two instances of the class mapped by this type for persistence "equality". 
    * Equality of the persistent state. 
    * 
    * @param x 
    * @param y 
    * @return boolean 
    */ 
    @Override 
    public boolean equals(Object x, Object y) throws HibernateException { 

     if(x== null){ 

      return y== null; 
     } 

     return x.equals(y); 
    } 

    /** 
    * Get a hashcode for the instance, consistent with persistence "equality" 
    */ 
    @Override 
    public int hashCode(Object x) throws HibernateException { 

     return x.hashCode(); 
    } 

    /** 
    * Retrieve an instance of the mapped class from a JDBC resultset. Implementors 
    * should handle possibility of null values. 
    * 
    * @param rs  a JDBC result set 
    * @param names the column names 
    * @param session 
    * @param owner the containing entity @return Object 
    * @throws org.hibernate.HibernateException 
    * 
    * @throws java.sql.SQLException 
    */ 
    @Override 
    public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException { 
     if(rs.getString(names[0]) == null){ 
      return null; 
     } 
     return rs.getString(names[0]); 
    } 

    /** 
    * Write an instance of the mapped class to a prepared statement. Implementors 
    * should handle possibility of null values. A multi-column type should be written 
    * to parameters starting from <tt>index</tt>. 
    * 
    * @param st  a JDBC prepared statement 
    * @param value the object to write 
    * @param index statement parameter index 
    * @param session 
    * @throws org.hibernate.HibernateException 
    * 
    * @throws java.sql.SQLException 
    */ 
    @Override 
    public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException { 
     if (value == null) { 
      st.setNull(index, Types.OTHER); 
      return; 
     } 

     st.setObject(index, value, Types.OTHER); 
    } 

    /** 
    * Return a deep copy of the persistent state, stopping at entities and at 
    * collections. It is not necessary to copy immutable objects, or null 
    * values, in which case it is safe to simply return the argument. 
    * 
    * @param value the object to be cloned, which may be null 
    * @return Object a copy 
    */ 
    @Override 
    public Object deepCopy(Object value) throws HibernateException { 

     return value; 
    } 

    /** 
    * Are objects of this type mutable? 
    * 
    * @return boolean 
    */ 
    @Override 
    public boolean isMutable() { 
     return true; 
    } 

    /** 
    * Transform the object into its cacheable representation. At the very least this 
    * method should perform a deep copy if the type is mutable. That may not be enough 
    * for some implementations, however; for example, associations must be cached as 
    * identifier values. (optional operation) 
    * 
    * @param value the object to be cached 
    * @return a cachable representation of the object 
    * @throws org.hibernate.HibernateException 
    * 
    */ 
    @Override 
    public Serializable disassemble(Object value) throws HibernateException { 
     return (String)this.deepCopy(value); 
    } 

    /** 
    * Reconstruct an object from the cacheable representation. At the very least this 
    * method should perform a deep copy if the type is mutable. (optional operation) 
    * 
    * @param cached the object to be cached 
    * @param owner the owner of the cached object 
    * @return a reconstructed object from the cachable representation 
    * @throws org.hibernate.HibernateException 
    * 
    */ 
    @Override 
    public Object assemble(Serializable cached, Object owner) throws HibernateException { 
     return this.deepCopy(cached); 
    } 

    /** 
    * During merge, replace the existing (target) value in the entity we are merging to 
    * with a new (original) value from the detached entity we are merging. For immutable 
    * objects, or null values, it is safe to simply return the first parameter. For 
    * mutable objects, it is safe to return a copy of the first parameter. For objects 
    * with component values, it might make sense to recursively replace component values. 
    * 
    * @param original the value from the detached entity being merged 
    * @param target the value in the managed entity 
    * @return the value to be merged 
    */ 
    @Override 
    public Object replace(Object original, Object target, Object owner) throws HibernateException { 
     return original; 
    } 
} 

現在剩下的就是註釋實體。把這樣的事情在實體的類聲明:

@TypeDefs({@TypeDef(name= "StringJsonObject", typeClass = StringJsonUserType.class)}) 

然後註釋屬性:

@Type(type = "StringJsonObject") 
public String getBar() { 
    return bar; 
} 

Hibernate會爲您創造與JSON類型列的照顧,並處理映射來回。將其他庫注入到用戶類型實現中以實現更高級的映射。

下面是一個簡單示例GitHub的項目,如果有人想玩弄它:

https://github.com/timfulmer/hibernate-postgres-jsontype

+0

感謝您的代碼示例! 我正在使用普通文本字段,但我可能會在將來採取您的方法 –

+0

感謝您花時間寫這篇文章。它強烈地使我感到挫敗,JPA沒有爲用戶定義的類型定義SPI鉤子,以獨立於JPA提供程序的方式編寫。 –

+2

不用擔心,我結束了代碼和這個頁面在我面前,想通了爲什麼不:)這可能是Java過程的缺點。通過解決棘手的問題,我們得到了一些非常好的想法,但要進入新的類型並添加一個像通用SPI這樣的好主意並不容易。我們留下了任何實現者,在這種情況下,Hibernate就位。 –

9

如果有人有興趣,你可以使用JPA 2.1 @Convert/@Converter功能與Hibernate。不過,您必須使用pgjdbc-ng JDBC驅動程序。這樣,您不必使用任何專有擴展名,方言和每個字段的自定義類型。

@javax.persistence.Converter 
public static class MyCustomConverter implements AttributeConverter<MuCustomClass, String> { 

    @Override 
    @NotNull 
    public String convertToDatabaseColumn(@NotNull MuCustomClass myCustomObject) { 
     ... 
    } 

    @Override 
    @NotNull 
    public MuCustomClass convertToEntityAttribute(@NotNull String databaseDataAsJSONString) { 
     ... 
    } 
} 

... 

@Convert(converter = MyCustomConverter.class) 
private MyCustomClass attribute; 
+0

這聽起來很有用 - 應該將哪些類型轉換爲能夠編寫JSON?它是還是其他類型? – myrosia

+0

謝謝 - 剛剛證實它適用於我(JPA 2.1,Hibernate 4.3.10,pgjdbc-ng 0.5,Postgres 9.3) – myrosia

+0

是否有可能在不指定@Column(columnDefinition =「json」)的情況下工作? Hibernate在沒有這個定義的情況下生成一個varchar(255)。 – tfranckiewicz

3

我有類似的問題在Postgres執行本地查詢(經由EntityManager的)時檢索JSON字段中(javax.persistence.PersistenceException:1111 org.hibernate.MappingException:否爲JDBC類型方言映射)投影,雖然實體類已用TypeDefs註釋。 在HQL中轉換的相同查詢執行時沒有任何問題。 爲了解決這個問題,我不得不修改JsonPostgreSQLDialect這樣:

public class JsonPostgreSQLDialect extends PostgreSQL9Dialect { 

public JsonPostgreSQLDialect() { 

    super(); 

    this.registerColumnType(Types.JAVA_OBJECT, "json"); 
    this.registerHibernateType(Types.OTHER, "myCustomType.StringJsonUserType"); 
} 

哪裏myCustomType.StringJsonUserType是實現JSON類型的類的類名(從上面,蒂姆·富爾默的答案)。

7

正如我在this article中解釋的那樣,使用Hibernate持久化JSON對象非常容易。

您不必手動創建所有這些類型的,你可以通過Maven的中央使用以下依賴簡單地得到 他們:

<dependency> 
    <groupId>com.vladmihalcea</groupId> 
    <artifactId>hibernate-types-52</artifactId> 
    <version>${hibernate-types.version}</version> 
</dependency> 

欲瞭解更多信息,請查看hibernate-types open-source project

現在來解釋它是如何工作的。

我寫了an article關於如何在PostgreSQL和MySQL上映射JSON對象。

對於PostgreSQL,你需要發送的JSON對象以二進制形式:

public class JsonBinaryType 
    extends AbstractSingleColumnStandardBasicType<Object> 
    implements DynamicParameterizedType { 

    public JsonBinaryType() { 
     super( 
      JsonBinarySqlTypeDescriptor.INSTANCE, 
      new JsonTypeDescriptor() 
     ); 
    } 

    public String getName() { 
     return "jsonb"; 
    } 

    @Override 
    public void setParameterValues(Properties parameters) { 
     ((JsonTypeDescriptor) getJavaTypeDescriptor()) 
      .setParameterValues(parameters); 
    } 

} 

JsonBinarySqlTypeDescriptor看起來是這樣的:

public class JsonBinarySqlTypeDescriptor 
    extends AbstractJsonSqlTypeDescriptor { 

    public static final JsonBinarySqlTypeDescriptor INSTANCE = 
     new JsonBinarySqlTypeDescriptor(); 

    @Override 
    public <X> ValueBinder<X> getBinder(
     final JavaTypeDescriptor<X> javaTypeDescriptor) { 
     return new BasicBinder<X>(javaTypeDescriptor, this) { 
      @Override 
      protected void doBind(
       PreparedStatement st, 
       X value, 
       int index, 
       WrapperOptions options) throws SQLException { 
       st.setObject(index, 
        javaTypeDescriptor.unwrap(
         value, JsonNode.class, options), getSqlType() 
       ); 
      } 

      @Override 
      protected void doBind(
       CallableStatement st, 
       X value, 
       String name, 
       WrapperOptions options) 
        throws SQLException { 
       st.setObject(name, 
        javaTypeDescriptor.unwrap(
         value, JsonNode.class, options), getSqlType() 
       ); 
      } 
     }; 
    } 
} 

JsonTypeDescriptor這樣的:

public class JsonTypeDescriptor 
     extends AbstractTypeDescriptor<Object> 
     implements DynamicParameterizedType { 

    private Class<?> jsonObjectClass; 

    @Override 
    public void setParameterValues(Properties parameters) { 
     jsonObjectClass = ((ParameterType) parameters.get(PARAMETER_TYPE)) 
      .getReturnedClass(); 

    } 

    public JsonTypeDescriptor() { 
     super(Object.class, new MutableMutabilityPlan<Object>() { 
      @Override 
      protected Object deepCopyNotNull(Object value) { 
       return JacksonUtil.clone(value); 
      } 
     }); 
    } 

    @Override 
    public boolean areEqual(Object one, Object another) { 
     if (one == another) { 
      return true; 
     } 
     if (one == null || another == null) { 
      return false; 
     } 
     return JacksonUtil.toJsonNode(JacksonUtil.toString(one)).equals(
       JacksonUtil.toJsonNode(JacksonUtil.toString(another))); 
    } 

    @Override 
    public String toString(Object value) { 
     return JacksonUtil.toString(value); 
    } 

    @Override 
    public Object fromString(String string) { 
     return JacksonUtil.fromString(string, jsonObjectClass); 
    } 

    @SuppressWarnings({ "unchecked" }) 
    @Override 
    public <X> X unwrap(Object value, Class<X> type, WrapperOptions options) { 
     if (value == null) { 
      return null; 
     } 
     if (String.class.isAssignableFrom(type)) { 
      return (X) toString(value); 
     } 
     if (Object.class.isAssignableFrom(type)) { 
      return (X) JacksonUtil.toJsonNode(toString(value)); 
     } 
     throw unknownUnwrap(type); 
    } 

    @Override 
    public <X> Object wrap(X value, WrapperOptions options) { 
     if (value == null) { 
      return null; 
     } 
     return fromString(value.toString()); 
    } 

} 

現在,您需要在任何一個類上聲明新類型在package-info.java包級descriptior VEL或:

@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) 

與實體映射將是這樣的:

@Type(type = "jsonb") 
@Column(columnDefinition = "json") 
private Location location; 

如果您在使用Hibernate 5或更高版本,然後JSON類型是registered automatically by Postgre92Dialect

否則,你需要自己註冊它:

public class PostgreSQLDialect extends PostgreSQL91Dialect { 

    public PostgreSQL92Dialect() { 
     super(); 
     this.registerColumnType(Types.JAVA_OBJECT, "json"); 
    } 
} 
+0

不錯的例子,但是這可以與一些通用的DAO一起使用,就像Spring Data JPA存儲庫一樣,可以在沒有本機查詢的情況下查詢數據,就像我們可以使用MongoDB一樣?我沒有找到任何有效答案或解決方案。是的,我們可以存儲這些數據,我們可以通過過濾RDBMS中的列來檢索它們,但目前爲止我無法通過JSONB coluns進行過濾。我希望我錯了,有這樣的解決方案。 – kensai

+0

是的,你可以。但是你需要使用Spring Data JPA支持的nativ查詢。 –

+0

我明白了,那實際上就是我的questin,如果我們可以沒有原生查詢,而只是通過對象方法。類似於MongoDB風格的@Document註釋。所以我認爲這是不是在PostgreSQL的情況下,唯一的解決方案是本機查詢 - >討厭:-),但感謝確認。 – kensai

2

我試過很多方法我在網上找到的,大多沒有工作,他們中的一些過於複雜。下面的一個適用於我,如果你對PostgreSQL類型驗證沒有這麼嚴格的要求,那就更簡單了。

製作有一個更容易做到這一點不涉及使用創建函數PostgreSQL的JDBC字符串類型爲不確定,像 <connection-url> jdbc:postgresql://localhost:test?stringtype=‌​unspecified </connect‌​ion-url>

0

WITH INOUT

CREATE TABLE jsontext(x json); 

INSERT INTO jsontext VALUES ($${"a":1}$$::text); 
ERROR: column "x" is of type json but expression is of type text 
LINE 1: INSERT INTO jsontext VALUES ($${"a":1}$$::text); 

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

INSERT INTO jsontext VALUES ($${"a":1}$$::text); 
INSERT 0 1 
相關問題