Большая часть приложений, которые мне встречались, хранят данные в 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
.
Контрольная сумма
Далее Liquibase читает главный changeLog, проверяя какие изменения уже были приняты, а какие надо выполнить.
После выполнения changeSet в таблицу databasechangelog
со всем прочим записывается MD5 хэш changeSet. Хэш высчитывается на основе нормализованного содержимого XML.
При следующем запуске Liquibase будет сверять вновь рассчитанные хэш суммы, со значениями в его таблице. Если вы изменили уже выполненный changeSet, то хэш сумма будет отличаться, и приложение упадет с ошибкой при старте.
После выполнения changeset нельзя изменить
Начало работы
Прежде чем запускать, нам необходимо создать главный changeLog и написать какие-нибудь changeSet, чтобы было что накатывать. Создадим простую табличку person
.
Организация скриптов
Создайте папку db
, а в ней папку changelog
. В этой папке создадим файл db.changelog-master.xml
. Это будет наш главный changeLog файл. Он будет содержать ссылки на другие changeLog файлы. Пока вставляем начальное содержимое:
Хорошей практикой считается создавать множество 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
Liquibase можно запускать несколькими способами, все они используются в зависимости от контекста:
- Запуск при помощи испольняемого файла
- Запуск при помощи Docker образа
- Запуск при старте SpringBoot или Quarkus сервиса
Запуск исполняемого файла
Начнем с запуска с помощью исполняемого файла, для этого скачайте Liquibase по ссылке ниже. Выберете пункт "Just the files", чтобы скачать jar файл.
Распаковав архив видим папку liquibase-4.9.1
. В ней много всего, опишу основные папки и файлы:
lib
– папка с зависимостями необходимыми для работыliquibase.jar
, в том числе драйверы БД.liquibase
– бинарный файл для запуска в среде Linux/MacOSliquibase.bat
– bat файл для запуска в среде Windows;liquibase.jar
– исполняемый .jar тут содержится вся логикаexamples
– папка с примерами скриптов миграций, но мы напишем свои.
Скопируйте нашу папку db
в папку liquibase-4.9.1
.
Файл liquibase.properties
Этот файл отвечает за конфигурацию запуска. Вместо того, чтобы запускать команду с кучей флагов, лучше создать один файл со всеми параметрами.
Создадим файл liquibase.properties
в папке liquibase-4.9.1
.
Не забудьте заменить значения соединения с базой данных на свои. В итоге у нас получается следующая структура файлов и папок:
.
└── 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
Далее просто скопируйте папку 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
со следующим содержанием:
Не забываем добавить include
на новый changelog.xml
в наш главный changeLog:
В итоге получаем простую схему БД
Посмотрим содержимое таблицу 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
Как я упоминал выше, не все изменения можно откатить в автоматическом режиме. Все операции по вставке данных не откатываются автоматически.
Добавим в наш 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 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. Узнали несколько способов запуска миграции. Также разобрались в популярных основных скриптах миграции БД, а также в способах отката изменений.