您的位置:首页 > 移动开发

Mapping postgreSQL JSON column to Hibernate value type

2014-05-09 14:06 489 查看
I have a table with a column of type json in my postgreSQL DB (9.2). I have a hard time to map this column to a JPA2 Entity field type.

I tried to use String but when I save the entity I get an exception that it can't convert character varying to json.

What is the correct value type to use when dealing with a JSON column?
@Entity
public class MyEntity {

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

public MyEntity() {
}
}


A simple workaround would be to define a text column.

5down
voteaccepted
PostgreSQL is excessively, annoyingly strict about data type conversions. It won't implicitly cast
text
even
to text-like values such as
xml
and
json
.

The strictly correct way to solve this problem is to write a custom Hibernate mapping type that uses the JDBC
setObject
method.
This can be a fair bit of hassle, so you might just want to make PostgreSQL less strict by creating a weaker cast.

As noted by @markdsievers in the comments and this
blog post, the original solution in this answer bypasses JSON validation. So it's not really what you want. It's safer to write:
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
tells PostgreSQL it can convert without being explicitly told to, allowing things like this to work:
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


Thanks to @markdsievers for pointing out the issue.

share|improve
this answer
edited Jan
23 at 4:16

answered Apr 13 '13 at 2:03




Craig Ringer

61.5k567120

thanks for the workaround! – Ümit Apr
17 '13 at 7:48
1
Worth reading the resulting blog
post of this answer. Inparticular the comment section highlights the dangers of this (allows invalid json) and the alternative / superior solution. – markdsievers Dec
11 '13 at 1:03
@markdsievers Thankyou. I've updated the post with a corrected solution. – Craig
Ringer Dec
11 '13 at 2:53
@CraigRinger No problem. Thank you for your prolific PG / JPA / JDBC contributions, many have been of great assistance to me. – markdsievers Dec
11 '13 at 3:25
14down
vote
If you're interested, here are a few code snippets to get the Hibernate custom user type in place. First extend the PostgreSQL dialect to tell it about the json type, thanks to Craig Ringer for the JAVA_OBJECT pointer:
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");
}
}


Next implement org.hibernate.usertype.UserType. The implementation below maps String values to the json database type, and vice-versa. Remember Strings are immutable in Java. A more complex implementation could be used to map custom Java beans to JSON stored
in the database as well.
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;
}
}


Now all that's left is annotating the entities. Put something like this at the entity's class declaration:
@TypeDefs( {@TypeDef( name= "StringJsonObject", typeClass = StringJsonUserType.class)})


Then annotate the property:
@Type(type = "StringJsonObject")
public String getBar() {
return bar;
}


Hibernate will take care of creating the column with json type for you, and handle the mapping back and forth. Inject additional libraries into the user type implementation for more advanced mapping.

Here's a quick sample GitHub project if anyone wants to play around with it:

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

share|improve
this answer
edited Feb
8 at 18:49

answered Apr 16 '13 at 23:45





Tim Fulmer

371210

thanks for the thorough code examples! I am using a normal text field but I might take your approach in future– Ümit Apr
17 '13 at 7:48
Thanks for taking the time to write this. It intensely frustrates
me that JPA doesn't define SPI hooks for user defined types to be written in a JPA-provider-independent way. – Craig
Ringer Apr
18 '13 at 0:25
2
No worries guys, I ended up with the code and this page in front of me and figured why not :) That might be the downside of the Java
process. We get some pretty well thought through solutions to tough problems, but it's not easy to go in and add a good idea like generic SPI for new types. We're left with whatever the implementers, Hibernate in this case, put in place. – Tim
Fulmer Apr
18 '13 at 17:24
2
there's a problem in your implementation code for nullSafeGet. Instead of of if(rs.wasNull()) you should do if(rs.getString(names[0])
== null). I'm not sure what rs.wasNull() does, but in my case it burned me by returning true, when the value I was looking for was in fact not null. – rtcarlson Sep
11 '13 at 15:22
1
This solution worked nicely with Hibernate 4.2.7 except when retrieving null from json columns with the error 'No Dialect mapping for
JDBC type: 1111'. However, adding the following line to the dialect class fixed it: this.registerHibernateType(Types.OTHER, "StringJsonUserType"); – oliverguenther Nov
18 '13 at 17:03
http://stackoverflow.com/questions/15974474/mapping-postgresql-json-column-to-hibernate-value-type
5down
voteaccepted
PostgreSQL is excessively, annoyingly strict about data type conversions. It won't implicitly cast
text
even
to text-like values such as
xml
and
json
.

The strictly correct way to solve this problem is to write a custom Hibernate mapping type that uses the JDBC
setObject
method.
This can be a fair bit of hassle, so you might just want to make PostgreSQL less strict by creating a weaker cast.

As noted by @markdsievers in the comments and this
blog post, the original solution in this answer bypasses JSON validation. So it's not really what you want. It's safer to write:
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
tells PostgreSQL it can convert without being explicitly told to, allowing things like this to work:
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


Thanks to @markdsievers for pointing out the issue.

share|improve
this answer
edited Jan
23 at 4:16

answered Apr 13 '13 at 2:03




Craig Ringer

61.5k567120

thanks for the workaround! – Ümit Apr
17 '13 at 7:48
1
Worth reading the resulting blog
post of this answer. Inparticular the comment section highlights the dangers of this (allows invalid json) and the alternative / superior solution. – markdsievers Dec
11 '13 at 1:03
@markdsievers Thankyou. I've updated the post with a corrected solution. – Craig
Ringer Dec
11 '13 at 2:53
@CraigRinger No problem. Thank you for your prolific PG / JPA / JDBC contributions, many have been of great assistance to me. – markdsievers Dec
11 '13 at 3:25
14down
vote
If you're interested, here are a few code snippets to get the Hibernate custom user type in place. First extend the PostgreSQL dialect to tell it about the json type, thanks to Craig Ringer for the JAVA_OBJECT pointer:
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");
}
}


Next implement org.hibernate.usertype.UserType. The implementation below maps String values to the json database type, and vice-versa. Remember Strings are immutable in Java. A more complex implementation could be used to map custom Java beans to JSON stored
in the database as well.
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;
}
}


Now all that's left is annotating the entities. Put something like this at the entity's class declaration:
@TypeDefs( {@TypeDef( name= "StringJsonObject", typeClass = StringJsonUserType.class)})


Then annotate the property:
@Type(type = "StringJsonObject")
public String getBar() {
return bar;
}


Hibernate will take care of creating the column with json type for you, and handle the mapping back and forth. Inject additional libraries into the user type implementation for more advanced mapping.

Here's a quick sample GitHub project if anyone wants to play around with it:

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

share|improve
this answer
edited Feb
8 at 18:49

answered Apr 16 '13 at 23:45





Tim Fulmer

371210

thanks for the thorough code examples! I am using a normal text field but I might take your approach in future– Ümit Apr
17 '13 at 7:48
Thanks for taking the time to write this. It intensely frustrates
me that JPA doesn't define SPI hooks for user defined types to be written in a JPA-provider-independent way. – Craig
Ringer Apr
18 '13 at 0:25
2
No worries guys, I ended up with the code and this page in front of me and figured why not :) That might be the downside of the Java
process. We get some pretty well thought through solutions to tough problems, but it's not easy to go in and add a good idea like generic SPI for new types. We're left with whatever the implementers, Hibernate in this case, put in place. – Tim
Fulmer Apr
18 '13 at 17:24
2
there's a problem in your implementation code for nullSafeGet. Instead of of if(rs.wasNull()) you should do if(rs.getString(names[0])
== null). I'm not sure what rs.wasNull() does, but in my case it burned me by returning true, when the value I was looking for was in fact not null. – rtcarlson Sep
11 '13 at 15:22
1
This solution worked nicely with Hibernate 4.2.7 except when retrieving null from json columns with the error 'No Dialect mapping for
JDBC type: 1111'. However, adding the following line to the dialect class fixed it: this.registerHibernateType(Types.OTHER, "StringJsonUserType"); – oliverguenther Nov
18 '13 at 17:03
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  json
相关文章推荐