Доброе время суток, уважаемое Хабр коммьюнити. В этой публикации я хотел бы показать несколько известных мне подходов к версионной миграции данных в контексте DTO. Примеры будут продемонстрированы на языке Java.
Ситуация
Вы являетесь бекенд разработчиком и разрабатываете серверное приложение, которое используется разного рода клиентами (клиентские приложения, не пользователи).
Само приложение состоит всего-лишь из одного домена - Пользователь, который в свою очередь состоит из числового идентификатора и номера телефона в виде строки.
Java
DTO:
@Getter public class UserDto { private Long id; @JsonProperty("phone_number") private String phoneNumber; }
Контроллер:
@RestController public class UserController { @GetMapping("/api/users/{user_id}") public UserDto getUser(@RequestParam("user_id") Long userId) { // Проверка существования пользователя и получение из БД // Вызов логики обработки // Конвертация модели в DTO return userDto; } }
HTTP
GET /api/users/{user_id} Host: api.host Accept: application/json { id: 0, phone_number: '+70123456789' }
В какой-то прекрасный момент Вам говорят, что хотелось бы использовать другой тип данных для номера телефона пользователя и возвращать его как число, а не текст как было прежде.
Конечно же, если команда:
состоит из волшебниковможет развернуть серверное и клиентское приложение одновременно, ничего при этом не сломав в продакшене;может себе позволить содержать сервера с разными версиями серверного приложения (api.host.v1, api.host.v2) и умеет их грамотно поддерживать.
В таком случае Вы постигли дзен и все у Вас хорошо)
Цель статьи - показать как можно разворачивать новые версии серверных приложений не затрагивая при этом клиентское приложение, чтобы оно смогло в достаточной мере отладить взаимодействие с новыми полями/типами данных прежде чем было опубликовано. Ну и не положить прод, конечно :)
Миграция через добавление нового поля
В этом случае просто в существующее DTO добавляется новое поле с постфиксом версии и старое помечается как устаревшее(в кодовой базе) для дальнейшего избавления от него. Старое поле продолжается поддерживаться до момента полного отказа в его использовании на стороне клиентского приложения.
Java
@Getter public class UserDto { private Long id; @Deprecated @JsonProperty("phone_number") private String deprecatedPhoneNumber; @JsonProperty("phone_number_v1") private Long phoneNumber; }
JSON
UserDto: { id: 0, phone_number: '+70123456789', phone_number_v1: 70123456789 }
Такой вариант хорошо подходит если команда занимается разработкой веб-приложений и Вам доступна быстрая миграция данных (имеется в виду, что вам не нужно очень долго поддерживать старые версии данных для клиентов). Если будет сильно смущать постфикс - можно сделать еще одну миграцию и просто удалить его и в дальнейшем избавиться от устаревших полей.
Миграция через создание новой версии объекта
В случае когда клиенты очень долго "переезжают" на новые версии приложений (типичный пример мобильной разработки, переезд на более новую версию может идти годами), то рано или поздно DTO превратиться в сущий ад если мигрировать данные через поле. Такой код будет сложно поддерживать, так как подобных миграций в период жизни приложения может быть достаточно много.
В данном случае можно выделить новый класс DTO.
Java
@Getter @Deprecated public class UserDto { private Long id; @JsonProperty("phone_number") private String phoneNumber; } @Getter public class UserDtoV1 { private Long id; @JsonProperty("phone_number") private Long phoneNumber; }
JSON
UserDto: { id: 0, phone_number: '+70123456789' } UserDtoV1: { id: 0, phone_number: 70123456789 }
При таком подходе избегается ситуация когда правок в классе становится настолько много, что начинаются трудности с взаимодействием и поддержкой, но при этом увеличивается число методов, которые отвечают за преобразование данных из БД в DTO. В таком случае можно воспользоваться шаблоном Фабрика и на каждую версию DTO создавать реализацию, которая будет отвечать за представление конкретной версии, чтобы выдавать клиентскому приложению версию DTO с которой он может работать.
Что насчет контроллеров?
В случае с контроллером есть тоже два варианта как можно реализовать версионность.
Так же есть хорошая статья которая ��писывает процессы версионирования конкретно API.
Миграция через новую версию HTTP метода
Java
@RestController public class UserController { @Deprecated @GetMapping("/api/users/{user_id}") public UserDto depreactedGetUser(@RequestParam("user_id") Long userId) {} @GetMapping("/api/users/{user_id}/v1") public UserDtoV1 getUser(@RequestParam("user_id") Long userId) {} }
HTTP
GET /api/users/{user_id} Host: api.host Accept: application/json { id: 0, phone_number: '+70123456789' } ------------------------------ GET /api/users/{user_id}/v1 Host: api.host Accept: application/json { id: 0, phone_number: 70123456789 }
Такой подход хорош при быстрой миграции, но поддерживать кучу таких методов может рано или поздно стать головной болью разработчика так как логика конвертации модели в DTO между такими методами будет "размазана".
Миграция через заголовок запроса
В данном случае передается заголовок Accept в котором указывается конкретная версия JSON объекта которую готов принимать клиент.
Пример:
Java
@RestController public class UserController { // Вместо возвращаемого типа Object можно создать UserBaseDto // в который можно вынести общие поля и использовать его как возвращаемый // тип метода @GetMapping("/api/users/{user_id}") public Object getUser( @RequestParam("user_id") Long userId, @RequestHeader("Accept") String acceptMimeType) { // Извлекаем версию конечного объекта из заголовка Accept // Формируем конечный объект return suitableUserDto; } }
HTTP
GET /api/users/{user_id} Host: api.host Accept: application/json Response object: { id: 0, phone_number: '+70123456789' } ------------------------------ GET /api/users/{user_id} Host: api.host Accept: application/json;v=1 Response object: { id: 0, phone_number: 70123456789 }
При данном подходе может затрудниться документирование системы так как один и тот же эндпоинт возвращает не конкретное представление, а сразу несколько. Если Ваша система документирования способна это описать, то проблем не возникнет и дальнейшие правки будут согласовыватьсся быстрее.
Заключение
Конечное решение о использовании того или иного подхода остается за разработчиком. Каждый описанный вариант подходит для конкретного случая.
P.S.
Было бы очень интересно узнать, как именно Ваша компания проводит миграцию данных)
