
Многие СУБД, помимо поддержки стандарта 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 поколения сборки мусора;
- Карьера и гибель объектов.
Регистрируйтесь по ссылке.
