Как использовать тип 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 поколения сборки мусора;
- Карьера и гибель объектов.
Регистрируйтесь по ссылке.