HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-02-26
1 <p>Из всех CRUD-операций, обновление сложнее всего реализовать правильно. В этом уроке вы узнаете, как правильно написать код для обновления сущностей с возможностью делать это частично.</p>
1 <p>Из всех CRUD-операций, обновление сложнее всего реализовать правильно. В этом уроке вы узнаете, как правильно написать код для обновления сущностей с возможностью делать это частично.</p>
2 <p>Предположим, что мы написали такой код для обновления сущности Post:</p>
2 <p>Предположим, что мы написали такой код для обновления сущности Post:</p>
3 <p>При этом код DTO выглядит так:</p>
3 <p>При этом код DTO выглядит так:</p>
4 <p>Код маппера выглядит так:</p>
4 <p>Код маппера выглядит так:</p>
5 <p>Представим, что мы отправляем такой JSON в этот API:</p>
5 <p>Представим, что мы отправляем такой JSON в этот API:</p>
6 <p>В коде выше мы не передали body, поэтому в DTO это свойство будет равно null. Это значит, что при копировании данных из DTO в объект post оригинальное значение будет стерто.</p>
6 <p>В коде выше мы не передали body, поэтому в DTO это свойство будет равно null. Это значит, что при копировании данных из DTO в объект post оригинальное значение будет стерто.</p>
7 <p>Такая реализация обновления работает только тогда, когда передаются все поля, указанные в DTO. На практике это порождает проблемы при одновременных обновлениях от разных клиентов - клиенты будут стирать данные друг друга.</p>
7 <p>Такая реализация обновления работает только тогда, когда передаются все поля, указанные в DTO. На практике это порождает проблемы при одновременных обновлениях от разных клиентов - клиенты будут стирать данные друг друга.</p>
8 <p>Кажется, что проблема решилась бы, если перед установкой значения мы проверили бы его на наличие null:</p>
8 <p>Кажется, что проблема решилась бы, если перед установкой значения мы проверили бы его на наличие null:</p>
9 <p>На самом деле, этот способ не сработает, потому что может быть ситуация, при которой мы действительно передали такое значение в JSON:</p>
9 <p>На самом деле, этот способ не сработает, потому что может быть ситуация, при которой мы действительно передали такое значение в JSON:</p>
10 <p>Эта ситуация возникает из-за того, что у свойств в объектах есть только два возможных значения:</p>
10 <p>Эта ситуация возникает из-за того, что у свойств в объектах есть только два возможных значения:</p>
11 <ul><li>Либо null</li>
11 <ul><li>Либо null</li>
12 <li>Либо какое-то конкретное значение</li>
12 <li>Либо какое-то конкретное значение</li>
13 </ul><p>Если в DTO оказался null, что это значит? Возможны два варианта:</p>
13 </ul><p>Если в DTO оказался null, что это значит? Возможны два варианта:</p>
14 <ul><li>Либо null действительно пришел в JSON снаружи</li>
14 <ul><li>Либо null действительно пришел в JSON снаружи</li>
15 <li>Либо свойство не было установлено вообще</li>
15 <li>Либо свойство не было установлено вообще</li>
16 </ul><p>В таких условиях мы не можем с уверенностью сказать, какой вариант правильный.</p>
16 </ul><p>В таких условиях мы не можем с уверенностью сказать, какой вариант правильный.</p>
17 <p>Решить эту проблему можно с помощью модуля<a>jackson-databind-nullable</a>в связке с MapStruct.</p>
17 <p>Решить эту проблему можно с помощью модуля<a>jackson-databind-nullable</a>в связке с MapStruct.</p>
18 <p>Обсудим принцип работы<em>jackson-databind-nullable</em>подробнее. Сначала мы оборачиваем в класс JsonNullable какое-то свойство, которое может отсутствовать. Дальше применяется следующая логика:</p>
18 <p>Обсудим принцип работы<em>jackson-databind-nullable</em>подробнее. Сначала мы оборачиваем в класс JsonNullable какое-то свойство, которое может отсутствовать. Дальше применяется следующая логика:</p>
19 <ul><li>Если в свойстве находится явный null, значение удалено явно</li>
19 <ul><li>Если в свойстве находится явный null, значение удалено явно</li>
20 <li>Если в свойстве находится JsonNullable.undefined(), значение не передано - его нужно игнорировать</li>
20 <li>Если в свойстве находится JsonNullable.undefined(), значение не передано - его нужно игнорировать</li>
21 <li>Если в свойстве находится реальное значение, обернутое в JsonNullable, то нужно его использовать</li>
21 <li>Если в свойстве находится реальное значение, обернутое в JsonNullable, то нужно его использовать</li>
22 </ul><p>Пройдем весь путь подключения этой библиотеки к проекту и MapStruct.</p>
22 </ul><p>Пройдем весь путь подключения этой библиотеки к проекту и MapStruct.</p>
23 <h2>Установка</h2>
23 <h2>Установка</h2>
24 <p>Для начала библиотеку нужно установить:</p>
24 <p>Для начала библиотеку нужно установить:</p>
25 <p>Сам модуль подключается к Jackson с помощью конфигурационного класса:</p>
25 <p>Сам модуль подключается к Jackson с помощью конфигурационного класса:</p>
26 <h2>Подключение к DTO</h2>
26 <h2>Подключение к DTO</h2>
27 <p>JacksonNullable подключается к опциональным свойствам DTO, то есть эти свойства могут быть пропущены при формировании JSON с клиентской стороны:</p>
27 <p>JacksonNullable подключается к опциональным свойствам DTO, то есть эти свойства могут быть пропущены при формировании JSON с клиентской стороны:</p>
28 <h2>Подключение к MapStruct</h2>
28 <h2>Подключение к MapStruct</h2>
29 <p>По умолчанию MapStruct ничего не знает о JsonNullable. Чтобы добавить нужную нам условную логику, проверяющую наличие реального значения, надо добавить специальный маппер:</p>
29 <p>По умолчанию MapStruct ничего не знает о JsonNullable. Чтобы добавить нужную нам условную логику, проверяющую наличие реального значения, надо добавить специальный маппер:</p>
30 <p>Это универсальный маппер, который можно подключить к любым другим мапперам. В нашей ситуации он понадобится для реализации маппера PostMapper:</p>
30 <p>Это универсальный маппер, который можно подключить к любым другим мапперам. В нашей ситуации он понадобится для реализации маппера PostMapper:</p>
31 <p>После такого изменения реализация метода update() в сгенерированном маппере значительно меняется:</p>
31 <p>После такого изменения реализация метода update() в сгенерированном маппере значительно меняется:</p>
32 <h2>Валидация</h2>
32 <h2>Валидация</h2>
33 <p>Как в таком случае использовать валидацию? Валидация же должна применяться к оригинальному значению, а не свойству в целом - иначе мы не сможем использовать null как значение. Хорошая новость в том, что это происходит автоматически. Все добавленные аннотации применяются не к обертке, а к тому, что находится внутри нее. Соответственно, если значение не передано, то и валидация не применяется. Значит, мы можем писать так:</p>
33 <p>Как в таком случае использовать валидацию? Валидация же должна применяться к оригинальному значению, а не свойству в целом - иначе мы не сможем использовать null как значение. Хорошая новость в том, что это происходит автоматически. Все добавленные аннотации применяются не к обертке, а к тому, что находится внутри нее. Соответственно, если значение не передано, то и валидация не применяется. Значит, мы можем писать так:</p>
34  
34