Spring Boot
2026-02-26 14:58 Diff

Из всех CRUD-операций, обновление сложнее всего реализовать правильно. В этом уроке вы узнаете, как правильно написать код для обновления сущностей с возможностью делать это частично.

Предположим, что мы написали такой код для обновления сущности Post:

При этом код DTO выглядит так:

Код маппера выглядит так:

Представим, что мы отправляем такой JSON в этот API:

В коде выше мы не передали body, поэтому в DTO это свойство будет равно null. Это значит, что при копировании данных из DTO в объект post оригинальное значение будет стерто.

Такая реализация обновления работает только тогда, когда передаются все поля, указанные в DTO. На практике это порождает проблемы при одновременных обновлениях от разных клиентов — клиенты будут стирать данные друг друга.

Кажется, что проблема решилась бы, если перед установкой значения мы проверили бы его на наличие null:

На самом деле, этот способ не сработает, потому что может быть ситуация, при которой мы действительно передали такое значение в JSON:

Эта ситуация возникает из-за того, что у свойств в объектах есть только два возможных значения:

  • Либо null
  • Либо какое-то конкретное значение

Если в DTO оказался null, что это значит? Возможны два варианта:

  • Либо null действительно пришел в JSON снаружи
  • Либо свойство не было установлено вообще

В таких условиях мы не можем с уверенностью сказать, какой вариант правильный.

Решить эту проблему можно с помощью модуля jackson-databind-nullable в связке с MapStruct.

Обсудим принцип работы jackson-databind-nullable подробнее. Сначала мы оборачиваем в класс JsonNullable какое-то свойство, которое может отсутствовать. Дальше применяется следующая логика:

  • Если в свойстве находится явный null, значение удалено явно
  • Если в свойстве находится JsonNullable.undefined(), значение не передано — его нужно игнорировать
  • Если в свойстве находится реальное значение, обернутое в JsonNullable, то нужно его использовать

Пройдем весь путь подключения этой библиотеки к проекту и MapStruct.

Установка

Для начала библиотеку нужно установить:

Сам модуль подключается к Jackson с помощью конфигурационного класса:

Подключение к DTO

JacksonNullable подключается к опциональным свойствам DTO, то есть эти свойства могут быть пропущены при формировании JSON с клиентской стороны:

Подключение к MapStruct

По умолчанию MapStruct ничего не знает о JsonNullable. Чтобы добавить нужную нам условную логику, проверяющую наличие реального значения, надо добавить специальный маппер:

Это универсальный маппер, который можно подключить к любым другим мапперам. В нашей ситуации он понадобится для реализации маппера PostMapper:

После такого изменения реализация метода update() в сгенерированном маппере значительно меняется:

Валидация

Как в таком случае использовать валидацию? Валидация же должна применяться к оригинальному значению, а не свойству в целом — иначе мы не сможем использовать null как значение. Хорошая новость в том, что это происходит автоматически. Все добавленные аннотации применяются не к обертке, а к тому, что находится внутри нее. Соответственно, если значение не передано, то и валидация не применяется. Значит, мы можем писать так: