Отображение столбца PostgreSQL JSON для типа значения Hibernate
У меня есть таблица со столбцом типа json в моей базе данных postgreSQL (9.2). Мне трудно сопоставить этот столбец с типом поля JPA2 Entity.
Я пытался использовать String, но когда я сохраняю объект, я получаю исключение, которое не может преобразовать символ, изменяющийся в json.
Каков правильный тип значения, который следует использовать при работе с столбцом JSON?
@Entity
public class MyEntity {
private String jsonPayload; // this maps to a json column
public MyEntity() {
}
}
Простым обходным решением будет определение текстового столбца.
Ответы
Ответ 1
Смотрите ошибку PgJDBС# 265.
PostgreSQL чрезмерно, досадно строгий в отношении преобразований типа данных. Он не будет явно лить text
даже в текстовые значения, такие как xml
и json
.
Строго правильный способ решения этой проблемы - написать настраиваемый тип сопоставления Hibernate, который использует метод JDBC setObject
. Это может быть довольно сложной проблемой, поэтому вы можете просто сделать PostgreSQL менее строгим, создав более слабый листинг.
Как отмечено @markdsievers в комментариях и этот пост в блоге, исходное решение в этом ответе обходит проверку 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 для указания проблемы.
Ответ 2
Если вам интересно, вот несколько фрагментов кода, чтобы получить собственный пользовательский тип Hibernate. Сначала расширьте диалоги PostgreSQL, чтобы рассказать о стиле json, благодаря 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 и наоборот. Помните, что Строки неизменны в Java. Более сложная реализация может быть использована для сопоставления пользовательского Java beans с 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
Ответ 3
Если кто-то заинтересован, вы можете использовать функциональность 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;
Ответ 4
Как я объяснил в этой статье, очень легко сохранить объект JSON с помощью Hibernate.
Вам не нужно создавать все эти типы вручную, вы можете просто получить их через Maven Central, используя следующую зависимость:
<dependency>
<groupId>com.vladmihalcea</groupId>
<artifactId>hibernate-types-52</artifactId>
<version>${hibernate-types.version}</version>
</dependency>
Для получения дополнительной информации ознакомьтесь с проектом с открытым исходным кодом типа hibernate.
Теперь, чтобы объяснить, как все это работает.
Я написал статью о том, как вы можете сопоставлять объекты JSON как на PostgreSQL, так и на MySQL.
Для 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 дескриптор уровня пакета:
@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
И отображение сущности будет выглядеть так:
@Type(type = "jsonb")
@Column(columnDefinition = "json")
private Location location;
Если вы используете Hibernate 5 или новее, то тип JSON
автоматически регистрируется Postgre92Dialect.
В противном случае вам необходимо зарегистрировать его самостоятельно:
public class PostgreSQLDialect extends PostgreSQL91Dialect {
public PostgreSQL92Dialect() {
super();
this.registerColumnType( Types.JAVA_OBJECT, "json" );
}
}
Ответ 5
У меня была аналогичная проблема с Postgres (javax.persistence.PersistenceException: org.hibernate.MappingException: Нет отображения диалектов для типа JDBC: 1111) при выполнении собственных запросов (через EntityManager), которые извлекали json-поля в проекции, хотя Entity класс был аннотирован с помощью 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-тип (сверху, ответ Тима Фулмера).
Ответ 6
Я пробовал много методов, которые я нашел в Интернете, большинство из них не работают, некоторые из них слишком сложны. Ниже я работаю для меня и намного проще, если у вас нет таких строгих требований к проверке типа PostgreSQL.
Введите тип строки jdbc PostgreSQL как неопределенный, например
<connection-url>
jdbc:postgresql://localhost:test?stringtype=unspecified
</connection-url>
Ответ 7
Это проще сделать, что не связано с созданием функции с помощью 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