将postgreSQL JSON列映射到Hibernate值types

我在postgreSQL数据库(9.2)有一个jsontypes的列表。 我很难将此列映射到JPA2实体字段types。

我试图使用string,但是当我保存实体时,我得到一个exception,它不能将字符变换为JSON。

处理JSON列时使用的正确的值types是什么?

@Entity public class MyEntity { private String jsonPayload; // this maps to a json column public MyEntity() { } } 

一个简单的解决方法是定义一个文本列。

见PgJDBC错误#265 。

PostgreSQL对数据types转换过分严格。 它不会隐式地将text为文本types的值,如xmljson

解决此问题的严格正确方法是编写一个使用JDBC setObject方法的自定义Hibernate映射types。 这可能有点麻烦,所以你可能只想通过创build一个较弱的强制转换来减lessPostgreSQL的严格性。

正如@markdsievers在评论和博客文章中指出的,这个答案中的原始解决scheme绕过了JSONvalidation。 所以这不是你想要的。 写更安全:

 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指出这个问题。

如果你有兴趣,下面是一些代码片段来获取Hibernate自定义用户types。 首先扩展PostgreSQL方言来告诉它关于jsontypes,这要感谢Craig Ringer的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数据库types,反之亦然。 记住string在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会负责为你创buildjsontypes的列,并且来回处理映射。 将其他库注入到用户types实现中以实现更高级的映射。

这里有一个快速示例GitHub项目,如果有人想玩它:

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

如果有人感兴趣,可以使用JPA 2.1 @Convert / @Converterfunction与Hibernate。 你将不得不使用pgjdbc-ng JDBC驱动程序。 这样您就不必使用任何专有扩展名,方言和每个字段的自定义types。

 @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; 

正如我在本文中解释的那样,使用Hibernate来保存JSON对象是非常容易的。

您不必手动创build所有这些types,只需通过Maven Central使用以下依赖项即可获取它们:

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

有关更多信息,请查看hibernate-type开源项目 。

现在,解释一切如何运作。

我写了一篇关于如何在PostgreSQL和MySQL上映射JSON对象的文章 。

对于PostgreSQL,您需要以二进制forms发送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包级别描述符中声明新types:

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

实体映射如下所示:

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

如果您使用Hibernate 5或更高版本,则JSONtypes由Postgre92Dialect自动注册 。

否则,您需要自行注册:

 public class PostgreSQLDialect extends PostgreSQL91Dialect { public PostgreSQL92Dialect() { super(); this.registerColumnType( Types.JAVA_OBJECT, "json" ); } } 

当执行在投影中检索到json字段的本机查询(通过EntityManager)时,Postgres(javax.persistence.PersistenceException:org.hibernate.MappingException:No Dialect mapping for JDBC type:1111)有类似的问题,尽pipeEntity类已经用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是实现jsontypes的类的类名(从上面,Tim Fulmer答案)。

我尝试了很多我在互联网上find的方法,其中大部分都不行,其中有些方法太复杂。 下面的一个适用于我,如果你对PostgreSQLtypesvalidation没有这么严格的要求,那就更简单了。

使PostgreSQL的jdbcstringtypes为未指定的,如<connection-url> jdbc:postgresql://localhost:test?stringtype=‌​unspecified </connect‌​ion-url>

有一个更容易做到这一点,不涉及使用WITH INOUT创build一个函数

 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