Введение
В структуре веб-сервисов типичным базовым набором операций над экземплярами сущностей(объектами) является CRUD (Create, Read, Update и Delete). Этим операциям в REST соответствуют HTTP методы POST, GET, PUT и DELETE. Но зачастую у разработчика возникает необходимость частичного изменения объекта, соответствующего HTTP методу PATCH. Смысл его состоит в том, чтобы на стороне сервера изменить только те поля объекта, которые были переданы в запросе. Причины для этого могут быть различные:
большое количество полей в сущности;
большая вероятность одновременного изменения одного и того же объекта при высокой нагрузке, в результате которого произойдет перезапись не только модифицируемых полей;
невозможность или более высокая сложность изменения полей в нескольких или всех объектах в хранилище(bulk update);
Эти и, возможно, другие причины побуждают разработчика реализовать стек операций по частичному изменению объекта.
Рассмотрим наиболее часто применяемые варианты решения задачи частичного обновления.
Использование обычного контроллера и DTO
Один из наиболее часто встречаемых вариантов реализации метода PATCH. В контроллере пришедший объект десериализуется в обычный DTO, и далее по стеку слоев приложения считается, что все поля в DTO со значением null не подлежат обработке.
К плюсам данного метода можно отнести "привычность" реализации.
К минусам относится во-первых потеря валидности значения null
для обработки (после десериализации мы не знаем отсутствовало ли это поле в передаваемом объекте или оно пришло нам со значением null
).
Вторым минусом является необходимость явной обработки каждого поля при конвертировании DTO в модель и далее по стеку в сущность. Особенно сильно это чувствуется в случае обработки сущностей с большим количеством полей, сложной структурой. Частично вторую проблему возможно решить с использованием ObjectMapper
(сериализация/десериализация POJO, аннотированных @JsonInclude(Include.NON_NULL) ) ,а так же библиотекой MapStruct, генерирующей код конвертеров.
Использование Map<String, Object> вместо POJO
Map<String, Object>
является универсальной структурой для представления данных и десериализации. Практически любой JSON объект может быть десериализован в эту структуру. Но, как мы можем понять из типов обобщения, мы теряем контроль типов на этапе компиляции (а соответственно и на этапе написания исходного кода в IDE).
К достоинствам этого метода можно отнести универсальность представления данных и возможность корректной обработки значения null
.
Недостатки же в данном случае глобальны: потеря контроля типов на этапе написания исходного кода и компиляции, необходимость явного приведения типов, высокая сложность рефакторинга, обнаружение практически всех ошибок, связанных с типами данных и именованием полей только в runtime(тесты или реальный запуск приложения).
Использование JSON Patch и JSON Merge Patch
JSON Patch и JSON Merge Patch являются стандартизованными и наиболее универсальными методами описания частичного изменения объекта. Спецификация Java EE содержит интерфейсы, описывающие работу с обоими этими форматами: JsonPatch и JsonMergePatch. Существуют реализации этих интерфейсов, одной из которых является библиотека json-patch. Оба формата кратко описаны в статье Michael Scharhag REST: Partial updates with PATCH.
Достоинства метода: стандартизация, универсальность, возможность реализовать не только изменение значения в объекте, но и добавление, удаление, перемещение, копирование и проверку значений полей в объекте.
К недостаткам метода можно отнести несколько большую сложность структуры данных, описывающих изменения в объекте и ее визуальное несоответствие типовому DTO объекта, более высокую сложность передачи данных об изменениях между слоями приложения, усложнение процесса валидации, усложненный процесс преобразования структуры описания изменений к запросам множественного обновления объектов в БД, etc.
Partial Update library
Основной целью создания библиотеки стало объединение положительных и исключение отрицательных сторон первых двух методов из описанных: использование классических DTO в фасадах и гибкости структуры Map<String, Object>
"под капотом".
Ключевыми элементами библиотеки являются интерфейс ChangeLogger и класс ChangeLoggerProducer.
Класс ChangeLoggerProducer
предназначен для создания "оберток" POJO, перехватывающих вызовы сеттеров и реализующих интерфейс ChangeLogger
для получения изменений, произведенных вызовами сеттеров в виде структуры Map<String, Object>
.
Для дальнейших примеров будут использоваться вот такие POJO:
public class UserModel {
private String login;
private String firstName;
private String lastName;
private String birthDate;
private String email;
private String phoneNumber;
}
@ChangeLogger
public class UserDto extends UserModel {
}
Вот пример работы с такой "оберткой":
ChangeLoggerProducer<UserDto> producer = new ChangeLoggerProducer<>(UserDto.class);
UserDto user = producer.produceEntity();
user.setLogin("userlogin");
user.setPhoneNumber("+123(45)678-90-12");
Map<String, Object> changeLog = ((ChangeLogger) user).changelog();
/*
changeLog in JSON notation will contains:
{
"login": "userlogin",
"phoneNumber": "+123(45)678-90-12"
}
*/
Суть "обертки" состоит в следующем: при вызове сеттера его имя добавляется в Set<String>
, при дальнейшем вызове метода Map<String, Object> changelog()
он вернет ассоциативный список, ключом в котором будет имя поля, а соответствующим ключу значением будет объект, возвращенный соответствующим геттером. В случае, если объект, возвращаемый геттером реализует интерфейс ChangeLogger
, то в значение поля пойдет результат вызова метода Map<String, Object> changelog()
.
Для сериализации/десериализации "оберток" реализован класс ChangeLoggerAnnotationIntrospector
. Это класс представляет собой Annotation Introspector для ObjectMapper
. Основной задачей класса является создание "обертки" при десериализации класса, аннотированного @ChangeLogger
аннотацией библиотеки и сериализацией результата вызова метода Map<String, Object> changelog()
вместо обычной сериализации всего объекта. Примеры использования ObjectMapper
с ChangeLoggerAnnotationIntrospector
приведены ниже.
Сериализация:
ObjectMapper mapper = new ObjectMapper.setAnnotationIntrospector(new ChangeLoggerAnnotationIntrospector());
ChangeLoggerProducer<UserDto> producer = new ChangeLoggerProducer<>(UserDto.class);
UserDto user = producer.produceEntity();
user.setLogin("userlogin");
user.setPhoneNumber("+123(45)678-90-12");
String result = mapper.writeValueAsString(user);
/*
result should be equal
"{\"login\": \"userlogin\",\"phoneNumber\": \"+123(45)678-90-12\"}"
*/
Десериализация:
ObjectMapper mapper = new ObjectMapper.setAnnotationIntrospector(new ChangeLoggerAnnotationIntrospector());
String source = "{\"login\": \"userlogin\",\"phoneNumber\": \"+123(45)678-90-12\"}";
UserDto user = mapper.readValue(source, UserDto.class);
Map<String, Object> changeLog = ((ChangeLogger) user).changelog();
/*
changeLog in JSON notation will contains:
{
"login": "userlogin",
"phoneNumber": "+123(45)678-90-12"
}
*/
Используя ObjectMapper
с ChangeLoggerAnnotationIntrospector
мы можем десериализовать пришедший нам в контроллер JSON с полями для частичного апдейта и далее передавать эти данные в подлежащие слои сервисов для реализации логики. В библиотеке присутствует инфраструктура для реализации мапперов DTO, Model, Entity с использованием "оберток". Пример полного стека приложения реализован в тестовом проекте Partial Update Example.
Итог
Partial Update library позволяет с рядом ограничений реализовать наиболее типичную задачу частичного изменения объекта, используя максимально близкий к типовому способ организации стека приложения. При этом универсальность ассоциативного списка объединена с сохранением контроля типов данных в процессе написания исходного кода и компиляции, что позволяет избежать большого числа ошибок в runtime.
В настоящее время функционал имеет ряд ограничений:
реализован только простейший маппинг "поле в поле", что не позволяет автоматизировать ситуации с разными именами одного и того же поля в DTO, Model, Entity;
не реализован модуль интеграции со Spring, в связи с чем для реализации сериализации/десериализации "оберток" DTO необходимо реализовать конфигурацию(как в примере), добавляющую
ChangeLoggerAnnotationIntrospector
в стандартныйObjectMapper
контроллера приложения;не реализованы утилиты формирования SQL/HQL запросов для bulk update операций с БД;
В последующих версиях планируется добавление недостающего функционала.
Формат данной статьи не позволяет более детально рассмотреть инфраструктуру для создания мапперов и показать применение библиотеки в типичном стеке приложения. В дальнейшем я могу более детально разобрать Partial Update Example и уделить больше внимания описанию внутренней реализации библиотеки.