image


В этой статье подробно объясняется, как решать проблемы, связанные с совместимостью баз данных при деплое. Мы расскажем, что может произойти с вашими приложениями на проде, если вы попытаетесь выполнить деплой без предварительной подготовки. Затем мы пройдемся по этапам жизненного цикла приложения, которые необходимы, чтобы иметь нулевое время простоя (прим. пер.: далее — zero downtime). Результатом наших операций будет применение обратно несовместимого изменения базы данных обратно совместимым способом.


Если вы хотите разобраться с примерами кода из статьи, вы их найдете на GitHub.


Введение


Zero downtime deployment


Что за мистический zero downtime deployment? Можно сказать, это когда ваше приложение развернуто так, что вы можете успешно вводить новую версию приложения на продакшн, в то время как пользователь не замечает его недоступности. С точки зрения пользователя и компании, это наилучший из возможных сценариев деплоя, поскольку таким образом можно вводить новые функции и устранять ошибки без перебоев в работе.


Как этого достичь? Есть несколько способов, вот один из них:


  • разверните версию №1 вашего сервиса
  • произведите миграцию БД
  • разверните версию № 2 вашего сервиса параллельно с версией № 1
  • как только вы увидите, что версия № 2 работает как надо, убирайте версию № 1
  • готово!

Легко, не правда ли? К сожалению, это не так просто, и мы позже подробно это рассмотрим. А сейчас давайте проверим еще один довольно распространенный процесс деплоя — blue green deployment.


Вы когда-нибудь слышали о blue green deployment? С Cloud Foundry это чрезвычайно легко сделать. Просто гляньте на эту статью, где мы описываем это более подробно. Кратко резюмируя, напомним, как делать blue green deployment:


  • обеспечить работу двух копий вашего production кода (“blue” и “green”);
  • направить весь трафик в blue среду, т.е. чтобы URL-адреса продакшена указывали туда;
  • разворачивать и тестировать все изменения приложения в green среде;
  • переключить URL-адреса с blue на green среду

Blue green deployment — это подход, который позволяет вам легко вводить новые функции, не переживая, что продакшн сломается. Это связано с тем фактом, что даже если что-то случится, вы можете легко откатиться на предыдущую среду, просто «щелкнув переключателем».


Прочитав все вышеперечисленное, вы можете задать вопрос: Какое отношение zero downtime имеет к Blue green деплою?


Что ж, у них довольно много общего, поскольку поддержка двух копий одной и той же среды требует двойных усилий для их обслуживания. Вот почему некоторые команды, как утверждает Martin Fowler, придерживаются вариации этого подхода:


другой вариант заключается в использовании той же БД, создавая сине-зеленые переключатели для web и domain layers. В таком подходе базы данных часто могут являться проблемой, особенно когда вам нужно изменить её схему для поддержки новой версии программного обеспечения.


И здесь мы подходим к главной проблеме в этой статье. База данных. Давайте еще раз взглянем на эту фразу.


произведите миграцию БД.


Теперь вы должны задать себе вопрос — что, если изменение базы данных обратно несовместимо? Разве моя первая версия приложения не сломается? На самом деле, именно это и случится...


Таким образом, даже несмотря на огромные преимущества zero downtime / blue green deployment, компании склонны следовать следующему более безопасному процессу деплоя своих приложений:


  • подготовить пакет с новой версией приложения
  • выключить запущенное приложение
  • запустить скрипты для миграции базы данных
  • развернуть и запустить новую версию приложения

В этой статье мы подробно опишем, как вы можете работать с базой данных и кодом, чтобы воспользоваться преимуществами zero downtime deployment.


Проблемы с базой данных


Если у вас есть stateless приложение, которое не хранит никаких данных в БД, вы можете получить zero downtime deployment сразу. К сожалению, большая часть программного обеспечения должна где-то хранить данные. Вот почему вы должны дважды подумать, прежде чем вносить какие-либо изменения в схему. Прежде чем мы углубимся в детали того, как изменить схему таким образом, чтобы стал возможным деплой без простоя, давайте сначала сосредоточимся на схеме управления версиями.


Схема управления версиями


В этой статье мы будем использовать Flyway в качестве инструмента для управления версиями (прим. пер.: речь идёт про миграции БД). Естественно, мы также напишем приложение Spring Boot, которое имеет встроенную поддержку Flyway и выполнит миграцию схемы во время настройки контекста приложения. При использовании Flyway вы можете хранить скрипты миграции в папке ваших проектов (по умолчанию в classpath:db/migration). Здесь вы можете увидеть пример таких файлов миграции


L-- db
 L-- migration
     +-- V1__init.sql
     +-- V2__Add_surname.sql
     +-- V3__Final_migration.sql
     L-- V4__Remove_lastname.sql

В этом примере мы видим 4 сценария миграции, которые, если они не были выполнены ранее, будут выполняться один за другим при запуске приложения. Давайте рассмотрим один из файлов (V1__init.sql) в качестве примера.


CREATE TABLE PERSON (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
first_name varchar(255) not null,
last_name varchar(255) not null
);

insert into PERSON (first_name, last_name) values ('Dave', 'Syer');

Все прекрасно говорит само за себя: вы можете использовать SQL, чтобы определить, как ваша база данных должна быть изменена. Для получения дополнительной информации о Spring Boot и Flyway ознакомьтесь с Spring Boot Docs.


Используя инструмент управления версиями со Spring Boot, вы получаете 2 больших преимущества:


  • вы отделяете изменения базы данных от изменений кода
  • миграция базы данных происходит вместе с выкаткой вашего приложения, т.е. ваш процесс деплоя упрощается

Решение проблем с базой данных


В следующем разделе статьи мы сосредоточимся на рассмотрении двух подходов к изменениям базы данных.


  • обратная несовместимость
  • обратная совместимость

Первый будет рассмотрен как предостережение, что не стоит производить zero downtime deployment без предварительной подготовки… Второй предлагает решение, как можно выполнить деплой без простоев и одновременно поддерживать обратную совместимость.


Наш проект, над которым мы будем работать, будет простым приложением Spring Boot Flyway, в котором есть Person с first_name и last_name в базе данных (прим. пер.: Person является таблицей, а first_name и last_name — это поля в ней). Мы хотим переименовать last_name в surname.


Допущения


Прежде чем мы углубимся в детали, необходимо обозначить пару допущений относительно наших приложений. Главным результатом, который мы хотим достичь, будет довольно простой процесс.


Заметка. Business PRO-TIP. Упрощение процессов может сэкономить вам много денег на поддержке (чем больше людей работает в вашей компании, тем больше денег вы можете сэкономить)!

Не надо делать откат базы данных


Это упрощает процесс деплоя (некоторые откаты базы данных практически невозможны, например откат удаления). Мы предпочитаем откатывать только приложения. Таким образом, даже если у вас разные базы данных (например, SQL и NoSQL), ваш deployment pipeline будет выглядеть одинаково.


Надо, чтобы ВСЕГДА была возможность откатить приложение на одну версию назад (не более)


Откат стоит производить только по необходимости. Если в текущей версии есть ошибка, которую нелегко устранить, мы должны иметь возможность вернуть последнюю рабочую версию. Мы предполагаем, что эта последняя рабочая версия является предыдущей. Поддержка совместимости кода и базы данных более чем для одной выкатки было бы чрезвычайно трудной и дорогостоящей.


Заметка. Для большей читабельности, в рамках данной статьи мы будем изменять мажорную версию приложения.

Шаг 1: Исходное состояние


Версия приложения: 1.0.0
Версия БД: v1


Комментарий


Это будет исходное состояние приложения.


Изменения БД


БД содержит last_name.


CREATE TABLE PERSON (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY,
    first_name varchar(255) not null,
    last_name varchar(255) not null
);

insert into PERSON (first_name, last_name) values ('Dave', 'Syer');

Изменения кода


Приложение сохраняет данные Person в last_name:


/*
 * Copyright 2012-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package sample.flyway;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Person {
    @Id
    @GeneratedValue
    private Long id;
    private String firstName;
    private String lastName;

    public String getFirstName() {
        return this.firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return this.lastName;
    }

    public void setLastName(String lastname) {
        this.lastName = lastname;
    }

    @Override
    public String toString() {
        return "Person [firstName=" + this.firstName + ", lastName=" + this.lastName
                + "]";
    }
}

Обратно несовместимое переименование столбца


Давайте рассмотрим пример, как изменить имя столбца:


Внимание. Следующий пример намеренно приведёт к поломке. Мы это показываем с целью продемонстрировать проблему совместимости базы данных.

Версия приложения: 2.0.0.BAD


Версия ДБ: v2bad


Комментарий


Текущие изменения НЕ позволяют нам запускать два экземпляра (старый и новый) одновременно. Таким образом, zero downtime deployment будет трудно достижим (если принять во внимание допущения, это фактически невозможно).


A/B-тестирование


Текущая ситуация такова, что у нас есть приложение версии 1.0.0, развернутое в проде, и БД v1. Мы должны развернуть второй экземпляр приложения, версии 2.0.0.BAD, и обновить базу данных до v2bad.


Шаги:


  1. развёрнут новый экземпляр приложения версии 2.0.0.BAD, которая обновляет базу данных до v2bad
  2. в базе данных v2bad столбец last_name больше не существует — его изменили на surname
  3. обновление базы данных и приложения прошло успешно, и некоторые экземпляры работают в 1.0.0, другие — в 2.0.0.BAD. Все связаны с БД v2bad
  4. все экземпляры версии 1.0.0 начнут выдавать ошибки, потому что они попытаются вставить данные в столбец last_name, которого больше нет
  5. все экземпляры версии 2.0.0.BAD будут работать без проблем

Как вы видите, если мы делаем обратно несовместимые изменения БД и приложения, A/B тестирование невозможно.


Откат приложения


Давайте предположим, что после попытки выполнить A/B deployment (прим. пер.: вероятно, тут автор имел в виду A/B тестирование) мы решили, что нам нужно откатить приложение до версии 1.0.0. Допустим, мы не хотим делать откат базы данных.


Шаги:


  1. мы останавливаем экземпляр приложения версии 2.0.0.BAD
  2. база данных все еще v2bad
  3. так как версия 1.0.0 не понимает, что такое surname, мы увидим ошибки
  4. ад вырвался на свободу, мы больше не можем вернуться

Как вы видите, если мы делаем обратно несовместимые изменения БД и приложения, мы не можем откатиться к предыдущей версии.


Логи исполнения скрипта


Backward incompatible scenario:

01) Run 1.0.0
02) Wait for the app (1.0.0) to boot
03) Generate a person by calling POST localhost:9991/person to version 1.0.0
04) Run 2.0.0.BAD
05) Wait for the app (2.0.0.BAD) to boot
06) Generate a person by calling POST localhost:9991/person to version 1.0.0 <-- this should fail
07) Generate a person by calling POST localhost:9992/person to version 2.0.0.BAD <-- this should pass

Starting app in version 1.0.0
Generate a person in version 1.0.0
Sending a post to 127.0.0.1:9991/person. This is the response:

{"firstName":"b73f639f-e176-4463-bf26-1135aace2f57","lastName":"b73f639f-e176-4463-bf26-1135aace2f57"}

Starting app in version 2.0.0.BAD
Generate a person in version 1.0.0
Sending a post to 127.0.0.1:9991/person. This is the response:

curl: (22) The requested URL returned error: 500 Internal Server Error

Generate a person in version 2.0.0.BAD
Sending a post to 127.0.0.1:9995/person. This is the response:

{"firstName":"e156be2e-06b6-4730-9c43-6e14cfcda125","surname":"e156be2e-06b6-4730-9c43-6e14cfcda125"}

Изменения БД


Скрипт миграции, который переименовывает last_name в surname


Исходный Flyway скрипт:


CREATE TABLE PERSON (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY,
    first_name varchar(255) not null,
    last_name varchar(255) not null
);

insert into PERSON (first_name, last_name) values ('Dave', 'Syer');

Скрипт, который переименовывает last_name.


-- This change is backward incompatible - you can't do A/B testing
ALTER TABLE PERSON CHANGE last_name surname VARCHAR;

Изменения кода


Мы изменили имя поля lastName на surname.


Переименование столбца обратно-совместимым способом


Это самая частая ситуация, с которой мы можем столкнуться. Нам нужно выполнить обратно несовместимые изменения. Мы уже доказали, что для деплоя без простоев мы не должны просто применять миграцию базы данных без дополнительных действий. В этом разделе статьи мы произведем 3 деплоя приложения вместе с миграциями базы данных, чтобы достичь желаемого результата и при этом сохранить обратную совместимость.


Заметка. Напомним, что у нас есть БД версии v1. Она содержит столбцы first_name и last_name. Мы должны изменить last_name на surname. У нас также есть приложение версии 1.0.0, которое пока не использует surname.

Шаг 2: Добавляем surname


Версия приложения: 2.0.0
Версия БД: v2


Комментарий


Добавляя новый столбец и копируя его содержимое, мы создаем обратно совместимые изменения БД. В то же время, если мы откатим JAR или у нас будет работающий старый JAR, он не сломается во время исполнения.


Выкатываем новую версию


Шаги:


  1. произведите миграцию БД, чтобы создать новый столбец surname. Теперь ваша БД версии v2
  2. скопируйте данные из last_name в surname. Обратите внимание, что если у вас много этих данных, вы должны рассмотреть возможность пакетной миграции!
  3. напишите код, где используются ОБА и новый, и старый столбец. Теперь ваше приложение версии 2.0.0
  4. прочитайте значение из столбца surname, если оно не null, или из last_name, если surname не задано. Вы можете удалить getLastName() из кода, так как он будет выдавать null при откате вашего приложения с 3.0.0 до 2.0.0.

Если вы используете Spring Boot Flyway, эти два шага будут выполнены во время старта версии 2.0.0 приложения. Если вы запускаете инструмент управления версиями базы данных вручную, вам придется сделать для этого два разных действия (сначала обновите версию db вручную, а затем разверните новое приложение).


Важно. Помните, что вновь созданный столбец НЕ ДОЛЖЕН быть NOT NULL. Если вы делаете откат, старое приложение не знает о новом столбце и не установит его во время Insert. Но если вы добавите это ограничение, и ваша БД будет v2, это потребует установки значения нового столбца. Что приведет к нарушениям ограничений.

Важно. Вам следует удалить метод getLastName(), поскольку в версии 3.0.0 в коде отсутствует понятие столбца last_name. Это означает, что там будут установлены null. Вы можете оставить метод и добавить проверки на null, но гораздо лучшим решением будет убедиться, что в логике getSurname() вы выбрали правильное ненулевое значение.

A/B-тестирование


Текущая ситуация такова, что у нас есть приложение версии 1.0.0, развернутое на проде, и БД в v1. Мы должны развернуть второй экземпляр приложения версии 2.0.0, который обновит базу данных до v2.


Шаги:


  1. развёрнут новый экземпляр приложения версии 2.0.0, которая обновляет базу данных до v2
  2. тем временем некоторые запросы обрабатывались экземплярами версии 1.0.0
  3. обновление прошло успешно, и у вас есть несколько работающих экземпляров приложения версии 1.0.0 и остальные версии 2.0.0. Все общаются с БД в v2
  4. версия 1.0.0 не использует в БД столбец surname, а версия 2.0.0 использует. Они не мешают друг другу, и ошибок не должно быть.
  5. версия 2.0.0 сохраняет данные как в старом, так и в новом столбце, что обеспечивает обратную совместимость

Важно. Если у вас есть какие-либо запросы, которые подсчитывают элементы на основе значений из старого / нового столбца, вы должны помнить, что теперь у вас есть дублирование значений (скорее всего, они все еще мигрируют). Например, если вы хотите посчитать количество пользователей, чья фамилия (как бы ни назывался столбец) начиналась с буквы A, то до завершения миграции данных (old > new столбец) у вас могут быть неконсистентные данные, если вы выполняете запрос к новому столбцу.

Откат приложения


Сейчас у нас есть приложение версии 2.0.0 и база данных в v2.


Шаги:


  1. откатите ваше приложение до версии 1.0.0.
  2. версия 1.0.0 не использует в БД столбец surname, поэтому откат должен быть успешным

Изменения DB


БД содержит столбец с именем last_name.


Исходный скрипт Flyway:


CREATE TABLE PERSON (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY,
    first_name varchar(255) not null,
    last_name varchar(255) not null
);

insert into PERSON (first_name, last_name) values ('Dave', 'Syer');

Скрипт добавления surname.


Внимание. Помните, что НЕЛЬЗЯ ДОБАВЛЯТЬ какие-либо ограничения NOT NULL в добавляемый столбец. Если вы откатываете JAR, старая версия не имеет понятия о добавленном столбце, и автоматически задаст ему значение NULL. В случае наличия такого ограничения старое приложение просто сломается.

-- NOTE: This field can't have the NOT NULL constraint cause if you rollback, the old version won't know about this field
-- and will always set it to NULL
ALTER TABLE PERSON ADD surname varchar(255);

-- WE'RE ASSUMING THAT IT'S A FAST MIGRATION - OTHERWISE WE WOULD HAVE TO MIGRATE IN BATCHES
UPDATE PERSON SET PERSON.surname = PERSON.last_name

Изменения кода


Мы сохраняем данные как в last_name, так и в surname. При этом читаем из last_name, поскольку этот столбец наиболее актуален. В процессе деплоя некоторые запросы могли быть обработаны экземпляром приложения, который еще не был обновлен.


/*
 * Copyright 2012-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package sample.flyway;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Person {
    @Id
    @GeneratedValue
    private Long id;
    private String firstName;
    private String lastName;
    private String surname;

    public String getFirstName() {
        return this.firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    /**
     * Reading from the new column if it's set. If not the from the old one.
     *
     * When migrating from version 1.0.0 -> 2.0.0 this can lead to a possibility that some data in
     * the surname column is not up to date (during the migration process lastName could have been updated).
     * In this case one can run yet another migration script after all applications have been deployed in the
     * new version to ensure that the surname field is updated.
     *
     * However it makes sense since when looking at the migration from 2.0.0 -> 3.0.0. In 3.0.0 we no longer
     * have a notion of lastName at all - so we don't update that column. If we rollback from 3.0.0 -> 2.0.0 if we
     * would be reading from lastName, then we would have very old data (since not a single datum was inserted
     * to lastName in version 3.0.0).
     */
    public String getSurname() {
        return this.surname != null ? this.surname : this.lastName;
    }

    /**
     * Storing both FIRST_NAME and SURNAME entries
     */
    public void setSurname(String surname) {
        this.lastName = surname;
        this.surname = surname;
    }

    @Override
    public String toString() {
        return "Person [firstName=" + this.firstName + ", lastName=" + this.lastName + ", surname=" + this.surname
                + "]";
    }
}

Шаг 3: Удаление last_name из кода


Версия приложения: 3.0.0


Версия ДБ:v3


Комментарий


Прим. пер.: По всей видимости, в исходной статье автором ошибочно был скопирован текст данного блока из шага 2. На данном шаге должны быть произведены изменения в коде приложения, направленные на удаление функционала, который использует столбец last_name.


Добавляя новый столбец и копируя его содержимое, мы создали обратно совместимые изменения БД. Также, если мы откатим JAR или у нас будет работающий старый JAR, он не сломается во время исполнения.


Откат приложения


В настоящее время у нас есть приложение версии 3.0.0 и база данных v3. Версия 3.0.0 не сохраняет данные в last_name. Это означает, что в surname хранится самая актуальная информация.


Шаги:


  1. откатите ваше приложение до версии 2.0.0.
  2. версия 2.0.0 использует и last_name и surname.
  3. версия 2.0.0 возьмёт surname, если он не нулевой, а иначе -last_name

Изменения БД


В БД нет структурных изменений. Исполняется следующий скрипт, который выполняет окончательную миграцию старых данных:


-- WE'RE ASSUMING THAT IT'S A FAST MIGRATION - OTHERWISE WE WOULD HAVE TO MIGRATE IN BATCHES
-- ALSO WE'RE NOT CHECKING IF WE'RE NOT OVERRIDING EXISTING ENTRIES. WE WOULD HAVE TO COMPARE
-- ENTRY VERSIONS TO ENSURE THAT IF THERE IS ALREADY AN ENTRY WITH A HIGHER VERSION NUMBER
-- WE WILL NOT OVERRIDE IT.
UPDATE PERSON SET PERSON.surname = PERSON.last_name;

-- DROPPING THE NOT NULL CONSTRAINT; OTHERWISE YOU WILL TRY TO INSERT NULL VALUE OF THE LAST_NAME
-- WITH A NOT_NULL CONSTRAINT.
ALTER TABLE PERSON MODIFY COLUMN last_name varchar(255) NULL DEFAULT NULL;

Изменения кода


Прим. пер.: Описание этого блока также было ошибочно скопировано автором из шага 2. В соответствии с логикой повествования статьи, изменения в коде на данном шаге должны быть направлены на удаление из него элементов, осуществляющих работу со столбцом last_name.


Мы храним данные как в last_name, так и в surname. Кроме того, мы читаем из столбца last_name, поскольку он наиболее актуален. В процессе развертывания некоторые запросы могут быть обработаны экземпляром, который еще не был обновлен.


/*
 * Copyright 2012-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package sample.flyway;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Person {
    @Id
    @GeneratedValue
    private Long id;
    private String firstName;
    private String surname;

    public String getFirstName() {
        return this.firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getSurname() {
        return this.surname;
    }

    public void setSurname(String lastname) {
        this.surname = lastname;
    }

    @Override
    public String toString() {
        return "Person [firstName=" + this.firstName + ", surname=" + this.surname
                + "]";
    }
}

Шаг 4: Удаление last_name из БД


Версия приложения: 4.0.0


Версия БД: v4


Комментарий


Из-за того, что код версии 3.0.0 не использовал столбец last_name, во время исполнения ничего плохого не произойдет, если мы откатимся до 3.0.0 после удаления столбца из базы данных.


Логи исполнения скрипта


We will do it in the following way:

01) Run 1.0.0
02) Wait for the app (1.0.0) to boot
03) Generate a person by calling POST localhost:9991/person to version 1.0.0
04) Run 2.0.0
05) Wait for the app (2.0.0) to boot
06) Generate a person by calling POST localhost:9991/person to version 1.0.0
07) Generate a person by calling POST localhost:9992/person to version 2.0.0
08) Kill app (1.0.0)
09) Run 3.0.0
10) Wait for the app (3.0.0) to boot
11) Generate a person by calling POST localhost:9992/person to version 2.0.0
12) Generate a person by calling POST localhost:9993/person to version 3.0.0
13) Kill app (3.0.0)
14) Run 4.0.0
15) Wait for the app (4.0.0) to boot
16) Generate a person by calling POST localhost:9993/person to version 3.0.0
17) Generate a person by calling POST localhost:9994/person to version 4.0.0

Starting app in version 1.0.0
Generate a person in version 1.0.0
Sending a post to 127.0.0.1:9991/person. This is the response:

{"firstName":"52b6e125-4a5c-429b-a47a-ef18bbc639d2","lastName":"52b6e125-4a5c-429b-a47a-ef18bbc639d2"}

Starting app in version 2.0.0

Generate a person in version 1.0.0
Sending a post to 127.0.0.1:9991/person. This is the response:

{"firstName":"e41ee756-4fa7-4737-b832-e28827a00deb","lastName":"e41ee756-4fa7-4737-b832-e28827a00deb"}

Generate a person in version 2.0.0
Sending a post to 127.0.0.1:9992/person. This is the response:

{"firstName":"0c1240f5-649a-4bc5-8aa9-cff855f3927f","lastName":"0c1240f5-649a-4bc5-8aa9-cff855f3927f","surname":"0c1240f5-649a-4bc5-8aa9-cff855f3927f"}

Killing app 1.0.0

Starting app in version 3.0.0

Generate a person in version 2.0.0
Sending a post to 127.0.0.1:9992/person. This is the response:
{"firstName":"74d84a9e-5f44-43b8-907c-148c6d26a71b","lastName":"74d84a9e-5f44-43b8-907c-148c6d26a71b","surname":"74d84a9e-5f44-43b8-907c-148c6d26a71b"}

Generate a person in version 3.0.0
Sending a post to 127.0.0.1:9993/person. This is the response:
{"firstName":"c6564dbe-9ab5-40ae-9077-8ae6668d5862","surname":"c6564dbe-9ab5-40ae-9077-8ae6668d5862"}

Killing app 2.0.0

Starting app in version 4.0.0

Generate a person in version 3.0.0
Sending a post to 127.0.0.1:9993/person. This is the response:

{"firstName":"cbe942fc-832e-45e9-a838-0fae25c10a51","surname":"cbe942fc-832e-45e9-a838-0fae25c10a51"}

Generate a person in version 4.0.0
Sending a post to 127.0.0.1:9994/person. This is the response:

{"firstName":"ff6857ce-9c41-413a-863e-358e2719bf88","surname":"ff6857ce-9c41-413a-863e-358e2719bf88"}

Изменения ДБ


Относительно v3 мы просто удаляем столбец last_name и добавляем отсутствующие ограничения.


-- REMOVE THE COLUMN
ALTER TABLE PERSON DROP last_name;

-- ADD CONSTRAINTS
UPDATE PERSON SET surname='' WHERE surname IS NULL;
ALTER TABLE PERSON ALTER COLUMN surname VARCHAR NOT NULL;

Изменения кода


Изменения в коде отсутствуют.


Вывод


Мы успешно применили обратно несовместимое изменение имени столбца, выполнив несколько обратно совместимых деплоев. Ниже сводка выполненных действий:


  1. деплой приложения версии 1.0.0 с v1 схемы БД (имя столбца = last_name)
  2. деплой приложения версии 2.0.0, которое сохраняет данные в last_name и surname. Приложение читает из last_name. БД находится в версии v2, содержащей столбцы как last_name, так и surname. surname является копией last_name. (ПРИМЕЧАНИЕ: этот столбец не должен иметь ограничение not null)
  3. деплой приложения версии 3.0.0, которое сохраняет данные только в surname и читает из surname. Что касается БД, то происходит последняя миграция last_name в surname. Также ограничение NOT NULL снимается с last_name. БД сейчас в версии v3
  4. деплой приложения версии 4.0.0 — в коде не производится никаких изменений. Деплой базы данных v4, которая удаляет last_name. Здесь вы можете добавить любые недостающие ограничения в БД.

Следуя этому подходу, вы всегда можете откатиться на одну версию назад, не ломая совместимость базы данных / приложения.


Код


Весь код, используемый в этой статье, доступен на Github. Ниже дополнительное описание.


Проекты


После клонирования репозитория, вы увидите следующую структуру папок.


+-- boot-flyway-v1              - 1.0.0 version of the app with v1 of the schema
+-- boot-flyway-v2              - 2.0.0 version of the app with v2 of the schema (backward-compatible - app can be rolled back)
+-- boot-flyway-v2-bad          - 2.0.0.BAD version of the app with v2bad of the schema (backward-incompatible - app cannot be rolled back)
+-- boot-flyway-v3              - 3.0.0 version of the app with v3 of the schema (app can be rolled back)
L-- boot-flyway-v4              - 4.0.0 version of the app with v4 of the schema (app can be rolled back)

Скрипты


Вы можете запустить сценарии, описанные в скриптах ниже, которые продемонстрируют обратно совместимые и несовместимые изменения в БД.


Чтобы увидеть случай с обратно совместимыми изменениями, запустите:


./scripts/scenario_backward_compatible.sh

А чтобы увидеть случай с обратно несовместимыми изменениями, запустите:


./scripts/scenario_backward_incompatible.sh

Spring Boot Sample Flyway


Все примеры взяты с Spring Boot Sample Flyway.


Вы можете взглянуть на http://localhost:8080/flyway, там список скриптов.


Этот пример также включает в себя консоль H2 (по адресу http://localhost:8080/h2-console), чтобы вы могли просматривать состояние базы данных (URL jdbc по умолчанию — jdbc:h2:mem:testdb).


Дополнительно



Также читайте другие статьи в нашем блоге: