Как использовать тип JSONB в PostgreSQL с Hibernate

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Многие СУБД, помимо поддержки стандарта SQL, предлагают дополнительную проприетарную функциональность. Одним из таких примеров является тип данных JSONB в PostgreSQL, позволяющий эффективно хранить JSON-документы.

Конечно, хранить JSON-документ можно и в виде простого текста — это входит в стандарт SQL и поддерживается Hibernate и JPA. Но тогда вам не будут доступны возможности PostgreSQL по обработке JSON, такие как валидация JSON и другие интересные функции и операторы. Хотя, вероятно, вы об этом уже знаете, раз читаете этот пост.

Если вы хотите использовать колонку типа JSONB с Hibernate 6, то у меня для вас отличные новости. В Hibernate 6 появился стандартный маппинг атрибутов сущностей на колонки JSON — необходимо только его активировать. К сожалению, Hibernate 4 и 5 не поддерживают JSON-маппинг, поэтому при их использовании придется реализовать UserType. Мы рассмотрим оба варианта.

Таблица базы данных и сущность

Перед реализацией UserType давайте быстро взглянем на таблицу базы данных и сущность.

Таблица будет очень простой из двух столбцов: id (первичный ключ) и jsonproperty типа JSONB.

CREATE TABLE myentity
(
  id bigint NOT NULL,
  jsonproperty jsonb,
  CONSTRAINT myentity_pkey PRIMARY KEY (id)
)

Сущность, отображаемая на таблицу, выглядит следующим образом.

@Entity
public class MyEntity {
  
    @Id
    @GeneratedValue
    private Long id;
  
    private MyJson jsonProperty;
      
    ...
}

Как видите, здесь нет ничего JSON-специфичного, кроме поля типа MyJson. Класс MyJson — это простой POJO с двумя свойствами.

public class MyJson implements Serializable {
  
    private String stringProp;
      
    private Long longProp;
  
    public String getStringProp() {
        return stringProp;
    }
  
    public void setStringProp(String stringProp) {
        this.stringProp = stringProp;
    }
  
    public Long getLongProp() {
        return longProp;
    }
  
    public void setLongProp(Long longProp) {
        this.longProp = longProp;
    }
}

Итак, что нужно сделать для сохранения свойства MyJson в JSONB? Ответ на этот вопрос зависит от версии Hibernate.

В Hibernate 4 и 5 необходимо написать кастомный маппинг. Не переживайте. Это не так уж сложно, как может показаться. Необходимо реализовать интерфейс UserType и зарегистрировать маппинг.

С Hibernate 6 все намного проще. Он поддерживает маппинг JSON из коробки. Давайте с него и начнем.

Маппинг JSONB в Hibernate 6

Благодаря поддержке JSON, появившейся в Hibernate 6, теперь нужно только аннотировать поле объекта аннотацией @JdbcTypeCode и установить тип SqlTypes.JSON. Hibernate обнаружит библиотеку для работы с JSON в classpath и будет использовать ее для сериализации и десериализации значения.

@Entity
public class MyEntity {
  
    @Id
    @GeneratedValue
    private Long id;
  
    @JdbcTypeCode(SqlTypes.JSON)
    private MyJson jsonProperty;
      
    ...
}

@JdbcTypeCode — это новая аннотация, которая была введена для поддержки маппинга новых типов. Начиная с Hibernate 6, вы можете определять маппинг Java и JDBC отдельно, аннотировав поле объекта аннотацией @JdbcTypeCode или @JavaType. Используя эти аннотации, вы можете указать один из стандартных маппингов Hibernate или свои реализации интерфейсов JavaTypeDescriptor или JdbcTypeDescriptor. Об этих интерфейсах я расскажу подробнее в другой статье, а здесь нам нужно активировать стандартный маппинг Hibernate.

После аннотирования поля сущности вы можете использовать сущность и ее атрибут в своем бизнес-коде. Пример использования приведен в конце статьи.

Маппинг JSONB в Hibernate 4 и 5

Как я упоминал ранее, для использования JSONB в PostgreSQL с Hibernate 4 или 5 вам необходим кастомный маппинг. Для этого реализуем интерфейс Hibernate UserType и зарегистрируем маппинг в кастомном диалекте.

Реализация UserType

Сначала создаем реализацию UserType, которая сопоставляет объект MyJson с JSON-документом и определяет SQL-тип для маппинга. Далее я приведу только отдельные важные моменты реализации MyJsonType. Полный исходный текст вы можете найти в репозитории GitHub.

В UserType надо реализовать методы sqlTypes и returnedClass, которые сообщают Hibernate SQL-тип и Java-класс, используемые для маппинга. В этом случае я использую Type.JAVA_OBJECT в качестве типа SQL и, конечно же, класс MyJson в качестве Java-класса.

public class MyJsonType implements UserType {
  
    @Override
    public int[] sqlTypes() {
        return new int[]{Types.JAVA_OBJECT};
    }
  
    @Override
    public Class<MyJson> returnedClass() {
        return MyJson.class;
    }
      
    ...
}

Затем нужно реализовать методы nullSafeGet и nullSafeSet, которые Hibernate использует для чтения и изменения значения.

Метод nullSafeGet нужен для маппинга значения, полученного из базы данных в класс Java. Для этого мы парсим JSON-документ в класс MyJson. Я использую ObjectMapper из Jackson, но вы можете использовать любой другой парсер JSON.

Метод nullSafeSet реализует маппинг класса MyJson в JSON-документ. Используя Jackson, это можно сделать с помощью того же ObjectMapper, что и в методе nullSafeGet.

@Override
public Object nullSafeGet(final ResultSet rs, final String[] names, final SessionImplementor session,
                          final Object owner) throws HibernateException, SQLException {
    final String cellContent = rs.getString(names[0]);
    if (cellContent == null) {
        return null;
    }
    try {
        final ObjectMapper mapper = new ObjectMapper();
        return mapper.readValue(cellContent.getBytes("UTF-8"), returnedClass());
    } catch (final Exception ex) {
        throw new RuntimeException("Failed to convert String to Invoice: " + ex.getMessage(), ex);
    }
}
  
@Override
public void nullSafeSet(final PreparedStatement ps, final Object value, final int idx,
                        final SessionImplementor session) throws HibernateException, SQLException {
    if (value == null) {
        ps.setNull(idx, Types.OTHER);
        return;
    }
    try {
        final ObjectMapper mapper = new ObjectMapper();
        final StringWriter w = new StringWriter();
        mapper.writeValue(w, value);
        w.flush();
        ps.setObject(idx, w.toString(), Types.OTHER);
    } catch (final Exception ex) {
        throw new RuntimeException("Failed to convert Invoice to String: " + ex.getMessage(), ex);
    }
}

Еще один важный метод, который необходимо реализовать, — это метод deepCopy, создающий глубокую копию объекта MyJson. Реализовать его можно очень просто — сериализовать и десериализовать объект MyJson.

@Override
public Object deepCopy(final Object value) throws HibernateException {
    try {
        // use serialization to create a deep copy
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(value);
        oos.flush();
        oos.close();
        bos.close();
          
        ByteArrayInputStream bais = new ByteArrayInputStream(bos.toByteArray());
        Object obj = new ObjectInputStream(bais).readObject();
        bais.close();
        return obj;
    } catch (ClassNotFoundException | IOException ex) {
        throw new HibernateException(ex);
    }
}

Регистрация UserType

Далее регистрируем нашу реализацию UserType в файле package-info.java с помощью аннотации @TypeDef.

@org.hibernate.annotations.TypeDef(name = "MyJsonType", typeClass = MyJsonType.class)
  
package org.thoughts.on.java.model;

Здесь тип MyJsonType связывается с именем "MyJsonType", которое далее мы можем использовать в аннотации @Type при маппинге сущности.

@Entity
public class MyEntity {
  
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id", updatable = false, nullable = false)
    private Long id;
  
    @Column
    @Type(type = "MyJsonType")
    private MyJson jsonProperty;
      
    ...
  
}

Теперь Hibernate будет использовать UserType MyJsonType для сохранения поля jsonproperty в базе данных. Однако остался еще один шаг.

Диалект Hibernate

Диалект PostgreSQL не поддерживает тип данных JSONB, его необходимо зарегистрировать. Для этого наследуемся от существующего диалекта и вызываем в конструкторе метод registerColumnType.

public class MyPostgreSQL94Dialect extends PostgreSQL94Dialect {
  
    public MyPostgreSQL94Dialect() {
        this.registerColumnType(Types.JAVA_OBJECT, "jsonb");
    }
}

Теперь можно сохранять объект MyJson в столбце JSONB.

Как использовать сущность с JSONB маппингом

Как вы поняли из статьи, реализация маппинга JSONB зависит от используемой версии Hibernate. Но это не влияет на бизнес-код, который использует сущность или ее атрибуты. Вы можете использовать сущность MyEntity и атрибут MyJson так же, как и любую другую сущность. И при миграции на Hibernate 6 это позволит заменить свою реализацию UserType на стандартный обработчик Hibernate.

В примере ниже показано использование метода EntityManager.find для получения сущности из базы данных и изменение атрибутов объекта MyJson.

MyEntity e = em.find(MyEntity.class, 10000L);
e.getJsonProperty().setStringProp("changed");
e.getJsonProperty().setLongProp(789L);

Если вам нужно реализовать выборку сущности на основе значений свойств внутри JSON-документа, то можно использовать нативные SQL-запросы с функциями и операторами PostgreSQL для работы с JSON.

MyEntity e = (MyEntity) em.createNativeQuery("SELECT * FROM myentity e WHERE e.jsonproperty->'longProp' = '456'", MyEntity.class).getSingleResult();

Резюме

PostgreSQL предлагает различные проприетарные типы данных, в том числе JSONB для хранения JSON-документов в базе данных.

Hibernate 6 поддерживает маппинг JSON из коробки. Вам нужно только активировать его, аннотировав атрибуты сущности аннотацией @JdbcTypeCode с типом SqlTypes.JSON.

В Hibernate 4 и 5 вы должны написать маппинг самостоятельно, реализовав интерфейс UserType, зарегистрировав его с помощью аннотации @TypeDef и создав диалект Hibernate, который регистрирует тип столбца.


Скоро состоится открытое занятие «Сборщик мусора в Java», на котором обсудим темы:
- Java Memory Model;
- 3 стадии и 2 поколения сборки мусора;
- Карьера и гибель объектов.
Регистрируйтесь по ссылке.

Источник: https://habr.com/ru/post/688714/


Интересные статьи

Интересные статьи

На днях Амит Капила закоммитил патч Масахико Савады, который позволяет выполнять очистку в параллельном режиме. Сама таблица по-прежнему очищается одним (ведущим) процессом, но для очистки индекс...
Для организации обработки потока задач используются очереди. Они нужны для накопления и распределения задач по исполнителям. Также очереди могут обеспечивать дополнительные требования к обработ...
Мы начали с вопросов, связанных с изоляцией, сделали отступление про организацию данных на низком уровне, подробно поговорили о версиях строк и о том, как из версий получаются снимки данных. З...
Сегодня мы поговорим о перспективах становления Битрикс-разработчика и об этапах этого пути. Статья не претендует на абсолютную истину, но даёт жизненные ориентиры.