Post

JSONB в PostgreSQL: область применения и ограничения

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 исчезает строгая схема на уровне базы, что увеличивает требования к валидации на уровне приложения. Запросы по вложенным данным становятся сложнее, а частичное обновление требует дополнительных операций. При чрезмерном использовании модель данных быстро деградирует и теряет управляемость.

This post is licensed under CC BY 4.0 by the author.