Как стать автором
Обновить

Версионная миграция данных в мире DTO

Уровень сложностиСредний
Время на прочтение4 мин
Количество просмотров5.3K

Доброе время суток, уважаемое Хабр коммьюнити. В этой публикации я хотел бы показать несколько известных мне подходов к версионной миграции данных в контексте 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.

Было бы очень интересно узнать, как именно Ваша компания проводит миграцию данных)

YouTube канал автора.

Микроблог автора.

Теги:
Хабы:
Всего голосов 11: ↑10 и ↓1+9
Комментарии11

Публикации