Tengo una tabla con una columna de tipo JSON en mi PostgreSQL DB (9.2). Me cuesta asignar esta columna a un tipo de campo de entidad JPA2.
Intenté usar String pero cuando guardo la entidad obtengo una excepción de que no puede convertir caracteres que varían a JSON.
¿Cuál es el tipo de valor correcto para usar cuando se trata de una columna JSON?
@Entity
public class MyEntity {
private String jsonPayload; // this maps to a json column
public MyEntity() {
}
}
Una solución alternativa sencilla sería definir una columna de texto.
Respuestas:
Consulte el error # 265 de PgJDBC .
PostgreSQL es excesivamente estricto con las conversiones de tipos de datos. No se convertirá implícitamente
text
ni siquiera en valores de texto comoxml
yjson
.La forma estrictamente correcta de resolver este problema es escribir un tipo de mapeo de Hibernate personalizado que use el
setObject
método JDBC . Esto puede ser un poco complicado, por lo que es posible que desee hacer que PostgreSQL sea menos estricto creando un reparto más débil.Como señaló @markdsievers en los comentarios y esta publicación de blog , la solución original en esta respuesta omite la validación JSON. Entonces no es realmente lo que quieres. Es más seguro escribir:
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
le dice a PostgreSQL que puede convertir sin que se le diga explícitamente, permitiendo que cosas como esta funcionen: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
Gracias a @markdsievers por señalar el problema.
fuente
cstring
conversión de todos modos, ¿no podría simplemente usarCREATE CAST (text AS json) WITH INOUT
?Si está interesado, aquí hay algunos fragmentos de código para implementar el tipo de usuario personalizado de Hibernate. Primero amplíe el dialecto de PostgreSQL para informarle sobre el tipo json, gracias a Craig Ringer por el puntero 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"); } }
A continuación, implemente org.hibernate.usertype.UserType. La siguiente implementación asigna valores de cadena al tipo de base de datos json y viceversa. Recuerde que las cadenas son inmutables en Java. También se podría utilizar una implementación más compleja para mapear beans Java personalizados a JSON almacenados en la base de datos.
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; } }
Ahora todo lo que queda es anotar las entidades. Pon algo como esto en la declaración de clase de la entidad:
@TypeDefs( {@TypeDef( name= "StringJsonObject", typeClass = StringJsonUserType.class)})
Luego anote la propiedad:
@Type(type = "StringJsonObject") public String getBar() { return bar; }
Hibernate se encargará de crear la columna con el tipo json por usted y manejará el mapeo de un lado a otro. Inyecte bibliotecas adicionales en la implementación del tipo de usuario para un mapeo más avanzado.
Aquí hay una muestra rápida de un proyecto de GitHub si alguien quiere jugar con él:
https://github.com/timfulmer/hibernate-postgres-jsontype
fuente
Dependencia de Maven
Lo primero que debe hacer es configurar la siguiente dependencia de Hibernate Type Maven en el
pom.xml
archivo de configuración de su proyecto :<dependency> <groupId>com.vladmihalcea</groupId> <artifactId>hibernate-types-52</artifactId> <version>${hibernate-types.version}</version> </dependency>
Modelo de dominio
Ahora, si está utilizando PostgreSQL, debe declarar el
JsonBinaryType
nivel de clase o en un descriptor de nivel de paquete package-info.java , como este:@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
Y el mapeo de entidades se verá así:
@Type(type = "jsonb") @Column(columnDefinition = "json") private Location location;
Si está utilizando Hibernate 5 o posterior, el
JSON
tipo es registrado automáticamente porPostgre92Dialect
.De lo contrario, debe registrarlo usted mismo:
public class PostgreSQLDialect extends PostgreSQL91Dialect { public PostgreSQL92Dialect() { super(); this.registerColumnType( Types.JAVA_OBJECT, "json" ); } }
Para MySQL, puede consultar este artículo para ver cómo puede mapear objetos JSON usando
JsonStringType
.fuente
En caso de que alguien esté interesado, puede usar la funcionalidad JPA 2.1
@Convert
/@Converter
con Hibernate. Sin embargo, tendría que usar el controlador JDBC pgjdbc-ng . De esta manera, no tiene que usar extensiones, dialectos ni tipos personalizados de propiedad por campo.@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;
fuente
Tuve un problema similar con Postgres (javax.persistence.PersistenceException: org.hibernate.MappingException: No hay asignación de dialecto para el tipo JDBC: 1111) al ejecutar consultas nativas (a través de EntityManager) que recuperaron campos json en la proyección aunque la clase Entity ha sido anotado con TypeDefs. La misma consulta traducida en HQL se ejecutó sin ningún problema. Para resolver esto, tuve que modificar JsonPostgreSQLDialect de esta manera:
public class JsonPostgreSQLDialect extends PostgreSQL9Dialect { public JsonPostgreSQLDialect() { super(); this.registerColumnType(Types.JAVA_OBJECT, "json"); this.registerHibernateType(Types.OTHER, "myCustomType.StringJsonUserType"); }
Donde myCustomType.StringJsonUserType es el nombre de clase de la clase que implementa el tipo json (desde arriba, la respuesta de Tim Fulmer).
fuente
Probé muchos métodos que encontré en Internet, la mayoría de ellos no funcionan, algunos de ellos son demasiado complejos. El siguiente funciona para mí y es mucho más simple si no tiene requisitos tan estrictos para la validación de tipo PostgreSQL.
Haga que el tipo de cadena jdbc de PostgreSQL no esté especificado, como
<connection-url> jdbc:postgresql://localhost:test?stringtype=unspecified </connection-url>
fuente
Hay una forma más fácil de hacer esto que no implica la creación de una función usando
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
fuente
Me estaba encontrando con esto y no quería habilitar cosas a través de la cadena de conexión y permitir conversiones implícitas. Al principio intenté usar @Type, pero como estoy usando un convertidor personalizado para serializar / deserializar un mapa hacia / desde JSON, no pude aplicar una anotación @Type. Resulta que solo necesitaba especificar columnDefinition = "json" en mi anotación @Column.
@Convert(converter = HashMapConverter.class) @Column(name = "extra_fields", columnDefinition = "json") private Map<String, String> extraFields;
fuente