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

Разберемся, как откатить изменения базы данных сделанных с помощью Liquibase.

· 9 мин.

В первой статье мы рассматривали что такое Liquibase, и его интеграцию со Spring Boot. Spring Boot позволяет накатывать изменения схемы БД при старте приложения, но откатить схему БД нельзя. Поэтому прежде чем говорить про откат, нам нужно научиться запускать Liquibase отдельно.

Мы расмотрим два варианта запуска Liquibase:

  • С помощью исполняемого файла
  • С помощью docker
ℹ️
Обращаю ваше внимание на используемые версии
MacOS – 12.1
Postgres – 13.2
Liquibase – 4.6.2

Запускаем Liquibase

Для начала скачайте Liquibase под свою ОС: Скачать.

Распаковав архив получаем папку liquibase-4.6.2. В ней много всего лишнего. Можете оставить только следующие файлы/папки

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

Файл liquibase.properties

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

url=jdbc:postgresql://localhost:5432/test
username=postgres
password=12345
changeLogFile=changelog/changelog.xml
classpath=lib/postgresql-42.2.15.jar
liquibase.hub.mode=off

Указываем данные для подключения к БД, путь до главного changeLog файла, а также указываем драйвер для базы данных. Драйвера лежат в папке lib. Установим параметр liquibase.hub.mode=off, чтобы отключить консольные сообщения от Liquibase.

Создание changeLog

Чтобы что-то откатывать, нужно сначала что-то накатить. В скаченной папке liquibase-4.6.2 создадим папку changelog. В ней мы и будем создавать наши changeLogFiles.

Так будет выглядить структура

Содержимое файлов changelog-v.1.0.0.xml и changelog-v.2.0.0.xml:

<databaseChangeLog
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">

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

    <include file="./changelog/v.1.0.0/create-tables.xml"/>

</databaseChangeLog>
<databaseChangeLog
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">

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

    <include file="./changelog/v.2.0.0/create-table-hero.xml"/>
    
</databaseChangeLog>

Обратите внимание на changeSet в начале каждого файла. О них мы еще поговорим.

Содержимое файла create-tables.xml

<databaseChangeLog
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.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)">
                <constraints nullable="false"/>
            </column>
        </createTable>
    </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="title" type="varchar(64)">
                <constraints nullable="false"/>
            </column>
            <column name="author" type="int">
                <constraints nullable="false" references="person(id)" foreignKeyName="fk_book_person_id"
                             deleteCascade="true"/>
            </column>
        </createTable>
    </changeSet>

</databaseChangeLog>

Содержимое файла create-table-hero.xml

<databaseChangeLog
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.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>

В итоге получается несложная схема БД:

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

🐱
Код проекта доступен на GitHub.

Перед запуском скриптов, не забудьте создать базу данных. После этого переходим в папку liquibase-4.6.2 и запускаем исполняемый файл:

./liquibase update
Starting Liquibase at 19:11:34 (version 4.6.2 #886 built at 2021-11-30 16:20+0000)
Liquibase Version: 4.6.2
Liquibase Community 4.6.2 by Liquibase
Liquibase command 'update' was executed successfully.

Видим, что все прошло успешно. Проверям нашу базу данных.

Все таблицы на месте

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

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

docker run --name liquibase --network host -v YOUR_PATH_TO_CHANGELOG:/liquibase/changelog liquibase/liquibase:4.6.1 --defaultsFile=/liquibase/changelog/liquibase.properties update
docker rm liquibase

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

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

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

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

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

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

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

Откат rollbackCount

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

./liquibase rollbackCount 1

В databasechangelog запись также пропадет, как-будто и не было этого changeSet. Поэтому можно снова запустить Liquibase update и связь между таблицами будет восстановлена.

./liquibase update

Откат rollback tag

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

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

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

  • Создание связи таблицы hero с book
  • Создание таблицы hero
  • Создание тега v.2.0.0
./liquibase rollback v.2.0.0
Остались только изменения changelog-v.1.0.0.xml

Ручные roolBack

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

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

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

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

И видим, что ничего не получилось. Автоматически вставку данных не отменить. Для отмены, необходимо в 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="title" value="Капитанская дочка"/>
            <column name="author" 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 , но можно было сделать и так:

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

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

Преимущество такого подхода в том, что если один из 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)">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <rollback/>
    </changeSet>

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

Заключение

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

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