Как использовать тип 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *