Partial Update library. Частичное обновление сущности в Java Web Services

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Введение

В структуре веб-сервисов типичным базовым набором операций над экземплярами сущностей(объектами) является 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 и уделить больше внимания описанию внутренней реализации библиотеки.

Источник: https://habr.com/ru/post/542818/


Интересные статьи

Интересные статьи

Изображения обычно хранятся как двоичные файлы, а файл Javascript по сути является обычным текстом. Оба типа файлов должны следовать собственным правилам: изображения имеют конкретн...
Ранее уже писал о том, что в 1С UI\UX дизайна нет. Эта статья про то, как мы с помощью таких технологий, как Java Script, React.js и Google Firebase решили сделать web-сервис, кот...
FYI: Первая часть. Бенчмарк пакетного конвейера Пакетный конвейер обрабатывает конечный объём сохранённых данных. Здесь нет потока результатов обработки, выходные данные агрегир...
Ваш сайт работает на 1С-Битрикс? Каждому клиенту вы даёте собственную скидку или назначаете персональную цену на товар? Со временем в вашей 1С сложилась непростая логика ценообразования и формирования...
В последнее время я часто слышу о том, что Java стала устаревшим языком, на котором сложно строить большие поддерживаемые приложения. В целом, я не согласен с этой точкой зрения. На мой взгляд, я...