JSONB в PostgreSQL: область применения и ограничения
Использование JSONB в реляционной базе выглядит спорно, если рассматривать его как замену нормализованной модели (как то: хранение связанных сущностей внутри одного JSON вместо отдельных таблиц и связей). В этом качестве он действительно приводит к ухудшению структуры данных. Однако есть задачи, где нормализация не дает приемлемого результата.
Один из таких случаев — хранение состояния объекта на момент времени.
Snapshot в микросервисах
В микросервисной архитектуре данные живут в разных сервисах и изменяются независимо. Например, сервис меню управляет блюдами, а сервис заказов фиксирует факты покупки.
При создании заказа необходимо сохранить характеристики позиции в том виде, в котором они были на момент оформления. Повторное получение этих данных из сервиса меню при чтении заказа не подходит: данные могут измениться или исчезнуть, а сам заказ должен оставаться неизменным.
По этой причине внутрь заказа сохраняется слепок данных. Это не ссылка на внешний объект, а полностью зафиксированная структура.
Например:
1
2
3
4
5
6
7
{
"name": "Pizza",
"price": 12.5,
"ingredients": [
{ "name": "cheese", "calories": 200 }
]
}
Такой объект сохраняется в JSONB-поле и далее рассматривается как часть самого заказа.
В PostgreSQL JSONB хранится в бинарном формате и допускает индексацию, поэтому его можно использовать не только как хранилище, но и для ограниченных выборок.
Работа с JSONB на уровне SQL
JSONB отличается от обычного текстового поля тем, что база понимает его структуру и предоставляет операции для работы с содержимым.
В PostgreSQL это позволяет:
- извлекать значения по ключам
- выполнять поиск по вложенным структурам
- использовать индексы для ускорения таких запросов
Пример запроса с использованием оператора включения:
1
2
3
SELECT *
FROM menu_items
WHERE ingredient_collection @> '[{"name": "Cheese"}]';
Запрос вернет записи, в которых в массиве ingredient_collection присутствует элемент с name = 'Cheese'.
В insert запросе JSONB-поле заполняется обычной JSON-строкой:
1
2
3
4
5
INSERT INTO menu_items (name, ingredient_collection)
VALUES (
'Pizza',
'[{"name": "Cheese"}, {"name": "Tomato"}]'
);
При вставке строка автоматически приводится к типу JSONB и сохраняется в бинарном формате.
Этот уровень работы с JSONB показывает, что он остается частью SQL-модели, а не просто текстовым хранилищем.
Таким образом, JSONB остается доступным для выборок, но запросы становятся заметно сложнее по сравнению с обычной реляционной моделью.
Маппинг в JPA
В JPA JSONB обычно мапится через кастомный тип. В случае Hibernate это часто делается с помощью библиотеки Hypersistence Utils.
Упрощенный пример сущности:
1
2
3
4
5
6
7
8
9
10
11
12
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue
private Long id;
@Type(JsonBinaryType.class)
@Column(name = "items", columnDefinition = "jsonb")
private OrderItemSnapshot items;
}
Где OrderItemSnapshot — обычный POJO:
1
2
3
4
5
6
public class OrderItemSnapshot {
private String name;
private BigDecimal price;
private List<Ingredient> ingredients;
}
На уровне ORM это выглядит как обычное поле, но в базе хранится единым JSONB-документом.
Пример содержимого строки в таблице:
1
SELECT id, items FROM orders WHERE id = 1;
Результат:
1
2
3
id | items
----+--------------------------------------------------
1 | {"name": "Pizza", "price": 12.5, "ingredients": [...]}
Реактивный стек (WebFlux / R2DBC)
В реактивном стеке (Spring WebFlux + R2DBC) маппинг выполняется без участия Hibernate, поэтому аннотация @Type для работы с JSONB недоступна.
В этом случае обычно используются конвертеры:
1
2
3
4
5
6
7
8
9
10
11
@ReadingConverter
public class JsonToOrderItemConverter
implements Converter<String, OrderItemSnapshot> {
// десериализация через ObjectMapper
}
@WritingConverter
public class OrderItemToJsonConverter
implements Converter<OrderItemSnapshot, String> {
// сериализация в JSON
}
И поле в сущности:
1
2
3
4
5
6
7
8
@Table("orders")
public class Order {
@Id
private Long id;
private OrderItemSnapshot items;
}
Фактически JSONB остается строкой на уровне драйвера, а преобразование берет на себя слой конвертеров.
Где это применимо
JSONB имеет смысл, когда данные представляют собой самостоятельный объект и не требуют активных связей с другими сущностями. В таких случаях он позволяет зафиксировать состояние и упростить чтение без сложной схемы.
Типичные сценарии — snapshot данных, вложенные структуры и расширяемые атрибуты.
Ограничения
При использовании JSONB исчезает строгая схема на уровне базы, что увеличивает требования к валидации на уровне приложения. Запросы по вложенным данным становятся сложнее, а частичное обновление требует дополнительных операций. При чрезмерном использовании модель данных быстро деградирует и теряет управляемость.