Зіставлення стовпця JSON PostgreSQL із властивістю сутності Hibernate


81

У мене є таблиця зі стовпцем типу JSON у моїй БД PostgreSQL (9.2). Мені важко зіставити цей стовпець із типом поля сутності JPA2.

Я намагався використовувати String, але коли я зберігаю сутність, я отримую виняток, що він не може перетворити символ, що змінюється в JSON.

Який правильний тип значення використовувати при роботі зі стовпцем JSON?

@Entity
public class MyEntity {

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

    public MyEntity() {
    }
}

Простим обхідним шляхом було б визначення текстового стовпця.


2
Я знаю , що це трохи старий, але подивіться на мою відповідь stackoverflow.com/a/26126168/1535995 на подібне питання
Sasa7812

vladmihalcea.com/... цей підручник досить простий
SGuru

Відповіді:


37

Див. Помилку PgJDBC # 265 .

PostgreSQL надмірно, надокучливо суворо ставиться до перетворень типів даних. Він не буде неявно передаватися textнавіть до текстових значень, таких як xmlіjson .

Суворо правильним способом вирішення цієї проблеми є написання власного типу відображення Hibernate, який використовує setObjectметод JDBC . Це може бути достатньо клопоту, тому ви можете просто захотіти зробити 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
Варто прочитати отриману публікацію в блозі цієї відповіді. Зокрема, у розділі коментарів висвітлюються небезпеки цього (допускає недійсний json) та альтернативного / вищого рішення.
markdsievers

@markdsievers Спасибі. Я оновив публікацію виправленим рішенням.
Крейг Рінгер,

@CraigRinger Немає проблем. Дякую за ваш плідний внесок PG / JPA / JDBC, багато хто мені дуже допомогли.
markdsievers

1
@CraigRinger Оскільки ви все cstringодно проходите перетворення, чи не могли б ви просто використовувати CREATE CAST (text AS json) WITH INOUT?
Нік Барнс,

@NickBarnes це рішення також прекрасно для мене спрацювало (і з того, що я бачив, воно не працює на недійсному JSON, як і слід). Дякую!
zeroDivisible

76

Якщо вам цікаво, ось декілька фрагментів коду, щоб встановити спеціальний тип користувача Hibernate. Спочатку розширіть діалект PostgreSQL, щоб розповісти йому про тип json, завдяки Крейгу Рінгеру за вказівник 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. Реалізація нижче відображає значення рядків до типу бази даних json і навпаки. Пам'ятайте, рядки є незмінними в Java. Більш складна реалізація може бути використана для зіставлення нестандартних компонентів Java у 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


2
Не хвилюйтеся, хлопці, я закінчив код і цю сторінку переді мною і зрозумів, чому ні :) Це може бути негативною стороною процесу Java. Ми отримуємо кілька досить продуманих рішень складних проблем, але це непросте рішення, щоб додати таку гарну ідею, як загальний SPI для нових типів. Нам залишається все, що впроваджувачі, Hibernate в даному випадку, встановлюються на місце.
Тім Фулмер

3
у вашому коді реалізації для nullSafeGet є проблема. Замість if (rs.wasNull ()) ви повинні зробити if (rs.getString (імена [0]) == null). Я не впевнений, що робить rs.wasNull (), але в моєму випадку це спалило мене, повернувши true, коли значення, яке я шукав, насправді не було нульовим.
rtcarlson

1
@rtcarlson Гарний улов! Вибачте, що вам довелося це пережити. Я оновив код вище.
Тім Фулмер

3
Це рішення добре працювало з Hibernate 4.2.7, крім випадків отримання null зі стовпців json з помилкою `` Немає відображення діалекту для типу JDBC: 1111 ''. Однак додавши наступний рядок до діалектного класу, це виправлено: this.registerHibernateType (Types.OTHER, "StringJsonUserType");
oliverguenther

7
Я не бачу жодного коду у зв’язаному проекті github ;-) До речі: Чи не було б корисно мати цей код як бібліотеку для повторного використання?
рю-

21

Це дуже поширене питання, тому я вирішив написати дуже докладну статтю про найкращий спосіб зіставлення типів стовпців JSON при використанні JPA та Hibernate.

Залежність Мейвена

Перше, що вам потрібно зробити, це встановити наступні залежності Hibernate Types Maven у pom.xmlфайлі конфігурації проекту :

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

Модель домену

Тепер, якщо ви використовуєте PostgreSQL, вам потрібно оголосити JsonBinaryTypeна рівні класу або в дескрипторі рівня пакету -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" );
    }
}

Для MySQL ви можете ознайомитися з цією статтею, щоб побачити, як можна зіставити об'єкти JSON за допомогою JsonStringType.


Гарний приклад, але чи можна це використовувати з деякими загальними DAO, як сховища Spring Data JPA для запиту даних без власних запитів, як ми можемо робити з MongoDB? Я не знайшов жодної дійсної відповіді чи рішення у цій справі. Так, ми можемо зберігати дані, і ми можемо отримати їх, фільтруючи стовпці в СУБД, але поки що я не можу фільтрувати за стовпцями JSONB. Я хотів би помилитися і таке рішення було.
kensai

Так, ти можеш. Але вам потрібно використовувати запити nativ, які також підтримуються Spring Data JPA.
Vlad Mihalcea

Я бачу, це був насправді мій квест, якщо ми можемо обійтися без власних запитів, але просто за допомогою методів об'єктів. Щось на зразок анотації @Document для стилю MongoDB. Тож я припускаю, що це не так далеко у випадку з PostgreSQL, і єдиним рішенням є власні запити -> противний :-), але дякую за підтвердження.
kensai

Було б непогано побачити в майбутньому щось на зразок сутності, яка реально представляє анотації таблиць та документів на полях типу json, і я можу використовувати сховища Spring, щоб робити речі CRUD на льоту. Думаю, що я створюю досить вдосконалений REST API для баз даних з Spring. Але з наявним JSON я стикаюся з досить несподіваними накладними витратами, тому мені також потрібно буде обробити кожен окремий документ із запитами на генерацію.
kensai

Ви можете використовувати Hibernate OGM з MongoDB, якщо JSON - це ваш єдиний магазин.
Vlad Mihalcea

12

Якщо хтось зацікавлений, ви можете використовувати JPA 2.1 @Convert/ @Converterфункціональність з Hibernate. Вам доведеться використовувати драйвер JDBC pgjdbc-ng . Таким чином, вам не потрібно використовувати власні розширення, діалекти та власні типи для кожного поля.

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

Це звучить корисно - між якими типами його слід конвертувати, щоб мати можливість писати JSON? Це <MyCustomClass, String> чи якийсь інший тип?
myrosia

Дякую - щойно перевірили, що це працює для мене (JPA 2.1, Hibernate 4.3.10, pgjdbc-ng 0.5, Postgres 9.3)
myrosia

Чи можна змусити його працювати, не вказавши в полі @Column (columnDefinition = "json")? Hibernate робить varchar (255) без цього визначення.
tfranckiewicz

Hibernate не може знати, який тип стовпця ви хочете там, але ви наполягаєте на тому, що відповідальність за сплячий режим є оновленням схеми бази даних. Тож я здогадуюсь, що він вибирає типовий.
vasily

3

У мене була подібна проблема з 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 (зверху, відповідь Тіма Фулмера).


3

Я спробував багато методів, які знайшов в Інтернеті, більшість з них не працюють, деякі з них занадто складні. Наведений нижче працює для мене і набагато простіший, якщо у вас немає таких суворих вимог до перевірки типу PostgreSQL.

Зробіть тип рядка PostgreSQL jdbc як невизначений, наприклад <connection-url> jdbc:postgresql://localhost:test?stringtype=‌​unspecified </connect‌​ion-url>


Дякую! Я використовував сплячі типи, але це набагато простіше! FYI ось документи щодо цього параметра jdbc.postgresql.org/documentation/83/connect.html
Джеймс,

2

Зробити це простіше, що не передбачає створення функції за допомогою 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

Дякую, використав це для відлиття варчара до ltree, працює чудово.
Володимир М.

1

Я стикався з цим і не хотів вмикати речі через рядок підключення та дозволяти неявні перетворення. Спочатку я намагався використовувати @Type, але оскільки я використовую спеціальний конвертер для серіалізації / десеріалізації карти в / з JSON, я не міг застосувати анотацію @Type. Виявляється, мені просто потрібно було вказати columnDefinition = "json" в моїй анотації @Column.

@Convert(converter = HashMapConverter.class)
@Column(name = "extra_fields", columnDefinition = "json")
private Map<String, String> extraFields;

3
Де ви визначили цей клас HashMapConverter. Як це виглядає.
пісок
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.