Миграции схемы базы данных с Liquibase

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

· 18 минуты на чтение
Миграции схемы базы данных с Liquibase

Большая часть приложений, которые мне встречались, хранят данные в SQL базе данных. Приложение публикуется на несколько стендов: стенд разработки, пре-прод и прод. А над приложением трудится команда разработчиков.

Такие корпоративные приложения сталкиваются с проблемой синхронизации схемы БД между контурами и командой разработчиков. Надо как-то передать изменения, которые вы внесли всем остальным и при этом не получить конфликты.

Эти проблемы решает система управления миграциями Liquibase. Это своего рода система контроля версий вашей базы данных. При этом она не зависит от используемой базы данных. Liquibase поддерживает множество БД, включая DB2, Apache Derby, MySQL, PostgreSQL, Oracle, Microsoft® SQL Server и прочие.

Список всех поддерживаемых БД можно посмотреть на сайте.
Спонсор поста

История изменения статьи

23.04.22: Обновил используемые версии. Также перенес сюда информацию из статьи про откат изменений.

Используемые версии

Liquibase: 4.9.1
Postgres: 13.2
SpringBoot: 2.6.7
MacOS: 12.3.1

Существует другие системы управления миграциями: Doctrine 2 migrations, Rails AR migrations, DBDeploy и т.д. Но некоторые из них платформо-зависимые, некоторые не обладают таким широким функционалом, как Liquibase.

Серьезный недостаток этих систем — невозможность применения некоторых изменений без потери данных, например, переименование столбца произойдет как две операции: drop + add, что приведет к потере данных.

Как работает Liquibase

Liquibase — кросс платформенное Java приложение, вы можете скачать JAR файл и запускать его на Windows, Mac или Linux.

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

ChangeLog

Изменения структуры базы данных записываются в файлы, которые называются changeLog. Эти файлы могут быть описаны в разных форматах: XML, YAML, JSON или SQL.

ChangeLog файлы могут быть произвольно включены друг в друга для лучшего управления. Подробнее об этом будет ниже в примерах.

Я являюсь ярым противником XML конфигураций, но в данном случае это самый удобный формат для записи.

ChangeSet

ChangeSet – это аналог коммита в системах контроля версий, таких как Git.

Каждый changeSet имеет составной идентификатор id, author и filename, который должен быть уникальным. ChangeSet может содержать одно или несколько изменений базы данных. Хорошей практикой считается одна команда для одного ChangeSet.

При первом запуске Liquibase создает две технические таблицы:

  • databasechangelog – Содержит список изменений схемы БД. Туда записываются уже выполненные changeSet.
  • databasechangelock – Используется для блокировки на время работы, чтобы гарантировать одновременную работу только одного экземпляра Liquibase.

Блокировка

Если несколько экземпляров Liquibase будут выполняться одновременно с одной и той же базой данных, вы получите конфликты. Это может произойти, если несколько разработчиков используют один и тот же экземпляр базы данных, или если в кластере несколько инстансовы, которые автоматически запускают Liquibase при запуске.

Для защиты от таких ситуаций Liquibase создает таблицу databasechangelock, в которой есть boolean поле locked. При запуске Liquibase проверяет его состояние, и если оно true, то ожидает смены на false.

Экстренно остановив выполнение программы в самом начале, может сложиться ситуация при котором Liquibase успеет поставить флаг, но не поменяет его на false.

Например при запуске в виде исполняемого файла все просто завсинет.

В логах SpringBoot приложения это будет более наглядно:

Заблокированная база данных

Чтобы исправить эту проблему, в таблице databasechangelock измените поле locked на false.

Таблица databasechangelock

Контрольная сумма

Далее Liquibase читает главный changeLog, проверяя какие изменения уже были приняты, а какие надо выполнить.

После выполнения changeSet в таблицу databasechangelog со всем прочим записывается MD5 хэш changeSet. Хэш высчитывается на основе нормализованного содержимого XML.

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

Пример вывода в лог сообщения об измененном changeSet
После выполнения changeset нельзя изменить

Начало работы

Прежде чем запускать, нам необходимо создать главный changeLog и написать какие-нибудь changeSet, чтобы было что накатывать. Создадим простую табличку person.

Организация скриптов

Создайте папку db, а в ней папку changelog. В этой папке создадим файл db.changelog-master.xml. Это будет наш главный changeLog файл. Он будет содержать ссылки на другие changeLog файлы. Пока вставляем начальное содержимое:

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.6.xsd">

    // Здесь будут ссылки на другие changeLog файлы
    
</databaseChangeLog>
db.changelog-master.xml

Хорошей практикой считается создавать множество changeLog и "включать" их в другие changeLog. Таким образом у вас будет множество файлов, а не один большой манускрипт, ведь changeSet-ов будет очень много со временем.

Проще разобраться на примере. Допустим у нас следующая версия приложения 1.0.0, поэтому мы создаем папку v.1.0.0 в папке db/changelog. Эта папка будет содержать только изменения схемы, которые мы будем делать для следующей версии нашего приложения. Эта папка будет содержать свой локальный главный changeLog файл, обычно я называю его просто changelog.xml.

Как только версия приложения меняется, мы создаем новую папку v.*.*.* и новые changeLog создаем уже в ней. И все созданные changelog.xml мы включаем в db.changelog-master.xml.

Итак, создайте папку v.1.0.0 в папке db/changelog, и создайте там файл changelog.xml. Теперь сделаем в основном changeLog файле ссылку на этот локальный changeLog. Для этого в файл добавляем следующую строку

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.6.xsd">

    <include file="v.1.0.0/changelog.xml" relativeToChangelogFile="true"/>

</databaseChangeLog>

Проще всего воспринимать тег include, как место, куда будет вставлено все содержимое того файла, который мы подключаем.

Также обратите внимание на атрибут relativeToChangelogFile="true". Он позволяет указывать путь относительно той папки, в которой располагается текущий changeLog.

Создание таблицы

Создадим таблицу person. Для этого в папке v.1.0.0 cоздадим новый файл create-table.xml, который будет содержать эту миграцию:

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.6.xsd">

    <changeSet id="create-table-person" author="uPagge">
        <createTable tableName="person">
            <column name="id" type="int" autoIncrement="true">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="first_name" type="varchar(64)"/>
        </createTable>
    </changeSet>

</databaseChangeLog>

Тег createTable содержит параметр tableName, который указывает имя новой таблицы. Внутри этого тега мы перечислили колонки, которые нам нужны.

Для колонок обязательно необходимо указать тип. Тип указывается в формате Liquibase, после чего он приводится для конкретной реализации БД.

Отдельного внимания заслуживает колонка id. Для нее мы задали автоинкремент, а так же в constraints  указали ограничения на колонку:

  • primaryKey="true" – колонка является первичным ключом таблицы.
  • nullable="false" – значения не могут быть NULL.
При использовании primaryKey параметр nullable не обязателен. Но если вы используете H2 для тестов, то у вас могут возникнуть проблемы из-за его отсутствия.

Теперь в файле changelog.xml необходимо указать тег include, ссылающийся на этот changeLog файл:

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.6.xsd">

    <changeSet id="add-tag-1.0.0" author="uPagge">
        <tagDatabase tag="v.1.0.0"/>
    </changeSet>

    <include file="create-table.xml" relativeToChangelogFile="true"/>

</databaseChangeLog>

Зачем нужен changeSet с тегом tagDatabase, я расскажу чуть позже, в разделе про откат миграций.

Запускаем Liquibase

😺
Проект на GitHub: example-uPagge/liquibase

Liquibase можно запускать несколькими способами, все они используются в зависимости от контекста:

  • Запуск при помощи испольняемого файла
  • Запуск при помощи Docker образа
  • Запуск при старте SpringBoot или Quarkus сервиса
Не забудьте перед этим создать базу данных.

Запуск исполняемого файла

Начнем с запуска с помощью исполняемого файла, для этого скачайте Liquibase по ссылке ниже. Выберете пункт "Just the files", чтобы скачать jar файл.

Liquibase | Open Source Version Control for Your Database
Liquibase Community is an open source project that helps millions of developers rapidly track, version, and deploy database schema changes.
Скачать с официального сайта ☝️

Распаковав архив видим папку liquibase-4.9.1. В ней много всего, опишу основные папки и файлы:

  • lib – папка с зависимостями необходимыми для работы liquibase.jar, в том числе драйверы БД.
  • liquibase – бинарный файл для запуска в среде Linux/MacOS
  • liquibase.bat – bat файл для запуска в среде Windows;
  • liquibase.jar – исполняемый .jar тут содержится вся логика
  • examples – папка с примерами скриптов миграций, но мы напишем свои.

Скопируйте нашу папку db в папку liquibase-4.9.1.

Файл liquibase.properties

Этот файл отвечает за конфигурацию запуска. Вместо того, чтобы запускать команду с кучей флагов, лучше создать один файл со всеми параметрами.

Создадим файл liquibase.properties в папке liquibase-4.9.1.

url=jdbc:postgresql://localhost:5432/liquibase_example
username=postgres
password=your_pass
changeLogFile=db/changelog/db.changelog-master.xml
liquibase.hub.mode=off
liquibase.properties

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

.
└── liquibase-4.9.1
    ├── db
    │   └── changelog
    │       ├── db.changelog-master.xml
    │       └── v.1.0.0
    │           ├── create-table.xml
    │           └── cumulative-changelog.xml
    ├── lib
    │   └── postgresql-42.3.2.jar
    ├── liquibase
    ├── liquibase.bat
    ├── liquibase.jar
    └── liquibase.properties

Для запуска миграции достаточно из папки liquibase-4.9.1 вызвать команду:

./liquibase update

В случае успеха в логе будет следующее сообщение:

Запуск с помощь докера

Можно выполнить скрипты используя докер образ. Для этого создадим небольшой Shell Script. Он будет запускать контейнер, который будет выполнять скрипты Liquibase, а потом удалять этот контейнер.

#!/bin/sh
docker run --name liquibase --network host -v YOUR_PATH_TO_CHANGELOG/db/changelog:/liquibase/db/changelog liquibase/liquibase:4.9.1 --defaultsFile=/liquibase/db/changelog/liquibase.properties update
docker rm liquibase

Не забудьте заменить YOUR_PATH_TO_CHANGELOG на путь до своей папки. Там должны лежать ваши changeLog файлы и liquibase.properties, который мы создавали для запуска с помощью исполняемого файла. То есть нужно скопировать liquibase.properties в папку changelog.

При запуске SpringBoot приложения

Чтобы добавить поддержку Liquibase в SpringBoot, нужно указать следующие зависимости в maven. Также не забудьте добавить драйвер базы данных, который вы используете.

<dependency>
    <groupId>org.liquibase</groupId>
    <artifactId>liquibase-core</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!--Драйвер БД-->
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>

Так же в файл application.yml укажем соединение с базой данных:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/liquibase_example
    username: postgres
    driver-class-name: org.postgresql.Driver
    password: 121314Ma
  liquibase:
    change-log: classpath:db/changelog/db.changelog-master.xml
  
Если вы используете Hibernate, то не забудьте отключить создание схемы БД.

Далее просто скопируйте папку db в папку resources.

В итоге должна получится вот такая структура папок.

Теперь достаточно запустить SpringBoot приложение. В процессе запуска в лог будут выведены сообщения от Liquibase.

Во всех трех случаях мы должны были получить нашу таблицу person:

Давайте изучим содержимое технической таблицы databasechangelog. Напомню, туда записываются выполненные скрипты. Это своего рода аналог git log. Для удобства я отображу эту таблицу в виде дерева

Итак, у нас здесь две записи, так как мы выполнили 2 changeSet. Первый это добавление tagDatabase, а второй это создание таблицы. Думаю назначение полей понятно по названиям, поэтому разбирать их не будем.

Рандомный блок

Скрипты миграций

Разберем скрипты миграций, которые вы будете использовать чаще всего.

Добавление колонки в таблицу

Давайте попробуем добавить новую колонку в таблицу просто изменив наш старый changeSet:

 <changeSet id="create-table-person" author="uPagge">
     <createTable tableName="person">
         <column name="id" type="int" autoIncrement="true">
             <constraints nullable="false" primaryKey="true"/>
         </column>
         <column name="first_name" type="varchar(64)"/>
         <column name="address" type="varchar(300)"/>
     </createTable>
 </changeSet>

Снова запустив миграцию, мы получим ошибку.

Если changeSet уже выполнился, и запись об этом есть в databasechangelog, то вы не можете просто изменить changeSet. Вы же не можете в git изменить уже опубликованный коммит.

В этом случае у вас три пути:

  • Создать новый changeSet с изменениями. [Рекомендуемый]
  • Выполнить откат средствами Liquibase.
  • Удалить запись о выполнении changeSet из databasechangelog. Не рекомендую этот вариант, если changeSet уже был выполнен на каком-то стенде. Этот вариант удобен при локальной разработке.

Вернем changeSet в его предыдущее состояние и создадим новый:

 <changeSet id="create-table-person" author="uPagge">
     <createTable tableName="person">
         <column name="id" type="int" autoIncrement="true">
             <constraints nullable="false" primaryKey="true"/>
         </column>
         <column name="name" type="varchar(64)"/>
     </createTable>
 </changeSet>

 <changeSet id="add-new-column-address" author="uPagge">
     <addColumn tableName="person">
         <column name="address" type="varchar(300)"/>
     </addColumn>
 </changeSet>

Запускаем миграцию. На этот раз успешно, новая колонка добавилась.

Связь с другой таблицей

Связь между таблицами довольно частое явление. Добавим новую таблицу Book и свяжем ее с таблицей Person. Создадим новый changeSet:

<changeSet id="create-table-book" author="uPagge">
    <createTable tableName="book">
        <column name="id" type="int" autoIncrement="true">
            <constraints nullable="false" primaryKey="true"/>
        </column>
        <column name="name" type="varchar(64)"/>
        <column name="author_id" type="int">
            <constraints foreignKeyName="book_author_id_person_id" references="person(id)"/>
        </column>
    </createTable>
</changeSet>

Теперь атрибут author_id связан с атрибутом id в таблице person. Обязательно укажите уникальный foreignKeyName. Я пользуюсь следующим правилом: имя_таблицы + имя_поля + имя_главной_таблицы + имя_поля_главной_таблицы.

Также мы можем включить каскадное удаление:

<constraints foreignKeyName="book_author_id_person_id" references="person(id)" deleteCascade="true"/>

Теперь, если автор книги будет удален, то книга тоже будет удалена. Если вам необходима операция каскадного обновления, то вам нужен второй способ связи с таблицей:

<changeSet id="create-table-book" author="uPagge">
    <createTable tableName="book">
        <column name="id" type="int" autoIncrement="true">
            <constraints nullable="false" primaryKey="true"/>
        </column>
        <column name="name" type="varchar(64)"/>
        <column name="author_id" type="int"/>
    </createTable>

    <addForeignKeyConstraint baseTableName="book" baseColumnNames="author_id"
                             constraintName="book_author_id_person_id"
                             referencedTableName="person" referencedColumnNames="id" onUpdate="CASCADE"/>
</changeSet>

Создание представления

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

<changeSet id="create-view-book-author" author="uPagge">
    <createView viewName="author_and_book">
        SELECT p.id as person_id,
               p.first_name as person_first_name,
               b.id as book_id,
               b.name as book_name
        FROM person p
                 LEFT JOIN book b on p.id = b.author_id
    </createView>
</changeSet>

Откат изменений

Теперь, когда мы разобрались с запуском Liquibase и основными скриптами миграции, можем переходить к откату изменений.

Спойлер: Судя по всему откат изменений это бесполезная возможность Liquibase, которой на практике никто не пользуется.

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

Многие операции Liquibase может откатить самостоятельно: создание таблицы, добавление колонки. Для некоторых changeSet необходимо написать скрипты отката. Мы начнем с примеров, где Liquibase может сам сделать откат, и постепенно дойдем до ручных откатов.

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

Но перед этим создадим еще одну таблицу. Только на этот раз представим, что наш сервис перешел на версию 2.0.0. Поэтому мы создадим новую папку v.2.0.0, а в ней новый changelog.xml и create-table-hero.xml со следующим содержанием:

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.6.xsd">

    <changeSet id="create-table-hero" author="uPagge">
        <createTable tableName="hero">
            <column name="id" type="int" autoIncrement="true">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="book_id" type="int">
                <constraints nullable="false"/>
            </column>
            <column name="name" type="varchar(64)">
                <constraints nullable="false"/>
            </column>
        </createTable>
    </changeSet>

    <changeSet id="create-fk" author="uPagge">
        <addForeignKeyConstraint baseTableName="hero" baseColumnNames="book_id"
                                 constraintName="hero_book_id"
                                 referencedTableName="book" referencedColumnNames="id"
                                 deleteCascade="true"/>
    </changeSet>

</databaseChangeLog>
create-table-hero.xml
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.6.xsd">

    <changeSet id="add-tag-2.0.0" author="uPagge">
        <tagDatabase tag="v.2.0.0"/>
    </changeSet>

    <include file="create-table-hero.xml" relativeToChangelogFile="true"/>

</databaseChangeLog>
cumulative-changelog.xml

Не забываем добавить include на новый changelog.xml в наш главный changeLog:

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.6.xsd">

    <include file="v.1.0.0/changelog.xml" relativeToChangelogFile="true"/>
    <include file="v.2.0.0/changelog.xml" relativeToChangelogFile="true"/>

</databaseChangeLog>
db.changelog-master.xml

В итоге получаем простую схему БД

Посмотрим содержимое таблицу databasechangelog, которая сохраняет историю выполнений changeLog. Для удобства я скрыл пустые колонки.

Откат rollbackCount

Если мы вызовем Liquibase с аргументом rollbackCount 1 вместо update, произойдет откат последнего changeSet: связь таблицы hero и book будет удалена.

./liquibase rollbackCount 1

В databasechangelog запись также пропадет, как-будто и не было этого changeSet.

Можно снова запустить ./liquibase update и связь между таблицами будет восстановлена.

Откат rollback tag

Откатывать по счетчику не удобно. Допустим вы хотите откатить все ваше приложение до предыдущей версии вместе со схемой БД. С этой предыдущей версии у вас уже было выполнено множество changeSet, и считать сколько их было дело не благодарное.

Поэтому в каждом changelog.xml я добавил tagDatabase. Это позволит откатить все изменения, которые были сделаны после этого тега, включая запись о создании этого тега. Таким образом можно откатить ваше приложение со схемой БД до необходимой версии.

Выполнив команду rollback v.2.0.0  мы откатим следующие изменения:

  • Создание связи таблицы hero с book
  • Создание таблицы hero
  • Создание тега v.2.0.0
./liquibase rollback v.2.0.0

Смотрим результат, действительно все изменения, включая создание тега v.2.0.0 были отменены.

Таблицы hero также больше не существует:

Ручные rollBack

Как я упоминал выше, не все изменения можно откатить в автоматическом режиме. Все операции по вставке данных не откатываются автоматически.

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

Добавим в наш changeLog changeSet со вставкой данных в таблицу:

<changeSet id="insert-into" author="uPagge">
    <insert tableName="person">
        <column name="first_name" value="Александр"/>
    </insert>
    <insert tableName="book">
        <column name="name" value="Капитанская дочка"/>
        <column name="author_id" value="1"/>
    </insert>
    <insert tableName="hero">
        <column name="name" value="Савельич"/>
        <column name="book_id" value="1"/>
    </insert>
</changeSet>

Попытаемся откатить этот changeSet:

./liquibase rollbackCount 1

И видим, что ничего не получилось. Автоматически вставку данных не отменить. Для отмены, необходимо в changeSet добавить раздел rollback.

Вначале статьи я говорил, что нельзя изменять уже выполненные changeSet. Но это правило не относится к разделу rollback. Его вы можете добавлять и изменять в changeSet даже после выполнения.

Поэтому просто добавляем в последний changeSet наш rollback:

<changeSet id="insert-into" author="uPagge">
    <insert tableName="person">
        <column name="first_name" value="Александр"/>
    </insert>
    <insert tableName="book">
        <column name="name" value="Капитанская дочка"/>
        <column name="author_id" value="1"/>
    </insert>
    <insert tableName="hero">
        <column name="name" value="Савельич"/>
        <column name="book_id" value="1"/>
    </insert>

    <rollback>
        <delete tableName="hero">
            <where>name = 'Савельич'</where>
        </delete>
        <delete tableName="book">
            <where>title = 'Капитанская дочка'</where>
        </delete>
        <delete tableName="hero">
            <where>first_name = 'Александр'</where>
        </delete>
    </rollback>
</changeSet>

Снова запускаем откат последнего изменения, и видим что все прошло успешно.

Мы указали все в одном теге rollback , но можно было сделать и так:

<rollback>
    <delete tableName="hero">
        <where>name = 'Савельич'</where>
    </delete>
</rollback>
<rollback>
    <delete tableName="book">
        <where>name = 'Капитанская дочка'</where>
    </delete>
</rollback>
<rollback>
    <delete tableName="person">
        <where>first_name = 'Александр'</where>
    </delete>
</rollback>

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

Также вы можете использовать чистый SQL:

<changeSet id="insert-into" author="uPagge">
    ... ... ... ... ...

    <rollback>
        <sql>
            DELETE FROM person
            WHERE first_name = 'Александр'
        </sql>
    </rollback>
</changeSet>

Проверка отката

Можно использовать команду updateTestingRollback, чтобы протестировать откат и накатить новые изменения.

./liquibase updateTestingRollback
Но работает эта команда очень странно. Сначала накатывает новые изменения, а потом последовательно выполняет откат, а затем снова запускает накат. Важно то, что эта команда не следит за сохранностью консистентности БД.

Запрет на откат changeSet

Возможно вы захотите, чтобы какой-либо changeSet остался после отката. То есть вы хотите откатить все изменения, кроме некоторых. Для этого добавьте пустой тег rollback.

<changeSet id="create-table-person" author="uPagge">
    <createTable tableName="person">
        <column name="id" type="int" autoIncrement="true">
            <constraints nullable="false" primaryKey="true"/>
        </column>
        <column name="first_name" type="varchar(64)"/>
    </createTable>
    <rollback/>
</changeSet>

В таком случае откатятся все изменения, кроме содержащих пустой тег rollback. Будьте аккуратны с этой функцией.

💪 Советы от бывалых

Познакомился я с Liquibase на своей стажировке в 2017. С тех пор я использую Liquibase в рабочих и пет-проектах. Мне уже проще написать changeSet, чем вспомнить SQL. Поэтому далее будет небольшой список рекомендаций, которые облегчат вам жизнь.

Организация ChangeSet

Я уже писал, что схема БД может довольно динамично меняться, особенно в начале создания приложения, поэтому мы ожидаем множество changeSet. И чтобы наш changeLog не превратился в длинный манускрипт, стоит создавать множество ChangeLog и включать их друг в друга. Далее я расскажу о своем подходе к организации changeLog структуры.

Я придерживаюсь следующего подхода:

  • Для каждой следующей версии приложения создаем папку в db/changelog. То есть, если текущая вреися приложения v.1.0.0, то создаем папку v.1.1.0.
  • В этой папке у нас будет локальный главный чейджлог-файл. Я называю их changelog.xml.
  • Когда вам необходимо внести набор изменений для схемы БД, то вы создаете отдельный changelog и включаете его в changelog.xml.
  • В главный changelog.xml мы подключаем все локальные changelog.xml.
На картинке немного устаревшая версия, но всю суть она передает.
Выпуск релиза

Во время выпуска релиза у вас могут оказаться запросы на слияния, которые затрагивают добавления новых changeSet. В этих ПР необходимо создать новую папку для новых changeLogs с номером нового релиза и перенести туда changeLogs для этих ПРов.

Правила именования

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

Вы можете придумать свои правила, но вот что предлагаю я:

  • Каждый changelog, кроме кумулятивных, начинается с текущей даты, а далее короткое описание всех изменений внутри. Например: 2020-03-08-create-tables.xml
  • Так же поступайте с id у changeSet. Например id="2020-03-08-create-table-person".

Не изменяйте данные

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

  • Кто-нибудь обязательно ошибется и ошибка уедет на тестовую среду, а откатывать придется вручную.
  • Идентификаторы к записям чаще всего генерируются автоматически, что может привести к дополнительным конфликтам.

Используйте XML

Иногда хочется «облегчить» жизнь и отказаться от XML, начав использовать более краткий DSL: groovy, yaml, json. Все это очень хорошо до тех пор, пока вам не захочется иметь:

  • Авто-дополнение в IDE
  • Автоматическую проверку формальной верности документа по схеме данных.

Используйте скрипты в формате XML, иногда используя SQL, если невозможно использовать XML.

Используйте remark

Разработчики стараются давать понятные имена для переменных, но дополнительное описание не будет лишним. Параметр remark позволяет добавлять описания к таблицам и полям.

<changeSet id="2021-02-22-create-person" author="uPagge">
    <createTable tableName="person" remarks="Пользователи системы">
        <column name="id" type="int" autoIncrement="true" remarks="Идентификатор">
            <constraints nullable="false" primaryKey="true"/>
        </column>
        <column name="name" type="varchar(64)" remarks="Имя пользователя">
            <constraints nullable="false"/>
        </column>
        <column name="telegram" type="int" remarks="Идентификатор в телеграмм">
            <constraints unique="true"/>
        </column>
    </createTable>
</changeSet>

Вот и все советы, которые пришли мне в голову. Удачи 🍀

Резюмирую

В этой статье мы немного узнали, как под капотом работает Liquibase. Узнали несколько способов запуска миграции. Также разобрались в популярных основных скриптах миграции БД, а также в способах отката изменений.

Struchkov Mark
Struchkov Mark
Задавайте вопросы, если что-то осталось не понятным👇