Содержание

Версионирование структуры БД при помощи Liquibase

451 2404 слов 12 минут

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

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

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

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

Изменения для БД записываются в формате понятном Liquibase, а уже он в свою очередь выполняет запросы к базе данных. Таким образом реализуется независимость от конкретной БД. Liquibase поддерживает 10 типов баз данных, включая DB2, Apache Derby, MySQL, PostgreSQL, Oracle, Microsoft® SQL Server, Sybase и HSQL. Список всех поддерживаемых БД можно посмотреть на сайте.

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

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

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

Для примера мы будем рассматривать работу со spring-boot приложением и PostgresSQL базой данных. Но вы должны знать, что liquibase можно использовать и отдельно в виде .jar файла. Вот так:

java -jar liquibase.jar --driver=com.mysql.jdbc.Driver--classpath=lib/mysql-connector-java-5.1.21-bin.jar --changeLogFile=/path/to/changelog.yaml --url="jdbc:mysql://localhost/application" --username=dbuser --password=secret update

Если вас мало волнует логика работы, то переходите к следующему разделу 👇.

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

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

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

ChangeSet – это аналог коммита в системах контроля версий, таких как Git. ChangeSet может содержать одно или несколько изменений базы данных. Хорошей практикой считается одна команда для одного ChangeSet.

Каждый changeSet имеет составной идентификатор id, author и filename, который должен быть уникальным.

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

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

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

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

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

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

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

Таблица databasechangelock

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

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

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

Ошибка хэша changeSet

После выполнения changeset нельзя изменить

И так у нас уже есть spring-boot приложение, в которое мы хотим добавить Liquibase.

Репозиторий с примерами из статьи на GitHub:
https://github.com/Example-uPagge/liqubase

Чтобы добавить поддержку Liquibase, нужно указать следующие зависимости в 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>

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

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

Теперь нам необходимо создать главный changelog. По умолчанию в spring-boot Liquibase ищет его в папке resources/db/changelog/db.changelog-master.yml. Как я уже говорил мы будем использовать XML формат.

Создаем файл resources/db/changelog/db.changelog-master.xml. И изменяем путь в application.yml:

spring:
  # .. .. .. .. ..
  liquibase:
    change-log: classpath:db/changelog/db.changelog-master.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">

    // сюда пишутся changeSets

</databaseChangeLog>

Чтобы быстро получить результат, мы создадим changeSet прямо в этом файле, а потом я расскажу почему так делать не стоит 😄

Создадим таблицу Person.

<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)"/>
        <column name="telegram_id" type="int">
            <constraints unique="true"/>
        </column>
    </createTable>
</changeSet>

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

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

Сопоставление типов Liquibase и различных БД

boolean
MySQLDatabase: BIT(1)
SQLiteDatabase: BOOLEAN
H2Database: BOOLEAN
PostgresDatabase: BOOLEAN
UnsupportedDatabase: BOOLEAN
DB2Database: SMALLINT
MSSQLDatabase: [bit]
OracleDatabase: NUMBER(1)
HsqlDatabase: BOOLEAN
FirebirdDatabase: SMALLINT
DerbyDatabase: SMALLINT
InformixDatabase: BOOLEAN
SybaseDatabase: BIT
SybaseASADatabase: BIT

tinyint
MySQLDatabase: TINYINT
SQLiteDatabase: TINYINT
H2Database: TINYINT
PostgresDatabase: SMALLINT
UnsupportedDatabase: TINYINT
DB2Database: SMALLINT
MSSQLDatabase: [tinyint]
OracleDatabase: NUMBER(3)
HsqlDatabase: TINYINT
FirebirdDatabase: SMALLINT
DerbyDatabase: SMALLINT
InformixDatabase: TINYINT
SybaseDatabase: TINYINT
SybaseASADatabase: TINYINT

int
MySQLDatabase: INT
SQLiteDatabase: INTEGER
H2Database: INT
PostgresDatabase: INT
UnsupportedDatabase: INT
DB2Database: INTEGER
MSSQLDatabase: [int]
OracleDatabase: INTEGER
HsqlDatabase: INT
FirebirdDatabase: INT
DerbyDatabase: INTEGER
InformixDatabase: INT
SybaseDatabase: INT
SybaseASADatabase: INT

mediumint
MySQLDatabase: MEDIUMINT
SQLiteDatabase: MEDIUMINT
H2Database: MEDIUMINT
PostgresDatabase: MEDIUMINT
UnsupportedDatabase: MEDIUMINT
DB2Database: MEDIUMINT
MSSQLDatabase: [int]
OracleDatabase: MEDIUMINT
HsqlDatabase: MEDIUMINT
FirebirdDatabase: MEDIUMINT
DerbyDatabase: MEDIUMINT
InformixDatabase: MEDIUMINT
SybaseDatabase: MEDIUMINT
SybaseASADatabase: MEDIUMINT

bigint
MySQLDatabase: BIGINT
SQLiteDatabase: BIGINT
H2Database: BIGINT
PostgresDatabase: BIGINT
UnsupportedDatabase: BIGINT
DB2Database: BIGINT
MSSQLDatabase: [bigint]
OracleDatabase: NUMBER(38, 0)
HsqlDatabase: BIGINT
FirebirdDatabase: BIGINT
DerbyDatabase: BIGINT
InformixDatabase: INT8
SybaseDatabase: BIGINT
SybaseASADatabase: BIGINT

float
MySQLDatabase: FLOAT
SQLiteDatabase: FLOAT
H2Database: FLOAT
PostgresDatabase: FLOAT
UnsupportedDatabase: FLOAT
DB2Database: FLOAT
MSSQLDatabase: float
OracleDatabase: FLOAT
HsqlDatabase: FLOAT
FirebirdDatabase: FLOAT
DerbyDatabase: FLOAT
InformixDatabase: FLOAT
SybaseDatabase: FLOAT
SybaseASADatabase: FLOAT

double
MySQLDatabase: DOUBLE
SQLiteDatabase: DOUBLE
H2Database: DOUBLE
PostgresDatabase: DOUBLE PRECISION
UnsupportedDatabase: DOUBLE
DB2Database: DOUBLE
MSSQLDatabase: float
OracleDatabase: FLOAT(24)
HsqlDatabase: DOUBLE
FirebirdDatabase: DOUBLE PRECISION
DerbyDatabase: DOUBLE
InformixDatabase: DOUBLE PRECISION
SybaseDatabase: DOUBLE
SybaseASADatabase: DOUBLE

decimal
MySQLDatabase: DECIMAL
SQLiteDatabase: DECIMAL
H2Database: DECIMAL
PostgresDatabase: DECIMAL
UnsupportedDatabase: DECIMAL
DB2Database: DECIMAL
MSSQLDatabase: [decimal](18, 0)
OracleDatabase: DECIMAL
HsqlDatabase: DECIMAL
FirebirdDatabase: DECIMAL
DerbyDatabase: DECIMAL
InformixDatabase: DECIMAL
SybaseDatabase: DECIMAL
SybaseASADatabase: DECIMAL

number
MySQLDatabase: numeric
SQLiteDatabase: NUMBER
H2Database: NUMBER
PostgresDatabase: numeric
UnsupportedDatabase: NUMBER
DB2Database: numeric
MSSQLDatabase: [numeric](18, 0)
OracleDatabase: NUMBER
HsqlDatabase: numeric
FirebirdDatabase: numeric
DerbyDatabase: numeric
InformixDatabase: numeric
SybaseDatabase: numeric
SybaseASADatabase: numeric

blob
MySQLDatabase: LONGBLOB
SQLiteDatabase: BLOB
H2Database: BLOB
PostgresDatabase: BYTEA
UnsupportedDatabase: BLOB
DB2Database: BLOB
MSSQLDatabase: varbinary
OracleDatabase: BLOB
HsqlDatabase: BLOB
FirebirdDatabase: BLOB
DerbyDatabase: BLOB
InformixDatabase: BLOB
SybaseDatabase: IMAGE
SybaseASADatabase: LONG BINARY

function
MySQLDatabase: FUNCTION
SQLiteDatabase: FUNCTION
H2Database: FUNCTION
PostgresDatabase: FUNCTION
UnsupportedDatabase: FUNCTION
DB2Database: FUNCTION
MSSQLDatabase: [function]
OracleDatabase: FUNCTION
HsqlDatabase: FUNCTION
FirebirdDatabase: FUNCTION
DerbyDatabase: FUNCTION
InformixDatabase: FUNCTION
SybaseDatabase: FUNCTION
SybaseASADatabase: FUNCTION

UNKNOWN
MySQLDatabase: UNKNOWN
SQLiteDatabase: UNKNOWN
H2Database: UNKNOWN
PostgresDatabase: UNKNOWN
UnsupportedDatabase: UNKNOWN
DB2Database: UNKNOWN
MSSQLDatabase: [UNKNOWN]
OracleDatabase: UNKNOWN
HsqlDatabase: UNKNOWN
FirebirdDatabase: UNKNOWN
DerbyDatabase: UNKNOWN
InformixDatabase: UNKNOWN
SybaseDatabase: UNKNOWN
SybaseASADatabase: UNKNOWN

datetime
MySQLDatabase: datetime
SQLiteDatabase: TEXT
H2Database: TIMESTAMP
PostgresDatabase: TIMESTAMP WITHOUT TIME ZONE
UnsupportedDatabase: datetime
DB2Database: TIMESTAMP
MSSQLDatabase: [datetime]
OracleDatabase: TIMESTAMP
HsqlDatabase: TIMESTAMP
FirebirdDatabase: TIMESTAMP
DerbyDatabase: TIMESTAMP
InformixDatabase: DATETIME YEAR TO FRACTION(5)
SybaseDatabase: datetime
SybaseASADatabase: datetime

time
MySQLDatabase: time
SQLiteDatabase: time
H2Database: time
PostgresDatabase: TIME WITHOUT TIME ZONE
UnsupportedDatabase: time
DB2Database: time
MSSQLDatabase: time
OracleDatabase: DATE
HsqlDatabase: time
FirebirdDatabase: time
DerbyDatabase: time
InformixDatabase: INTERVAL HOUR TO FRACTION(5)
SybaseDatabase: time
SybaseASADatabase: time

timestamp
MySQLDatabase: timestamp
SQLiteDatabase: TEXT
H2Database: TIMESTAMP
PostgresDatabase: TIMESTAMP WITHOUT TIME ZONE
UnsupportedDatabase: timestamp
DB2Database: timestamp
MSSQLDatabase: [datetime]
OracleDatabase: TIMESTAMP
HsqlDatabase: TIMESTAMP
FirebirdDatabase: TIMESTAMP
DerbyDatabase: TIMESTAMP
InformixDatabase: DATETIME YEAR TO FRACTION(5)
SybaseDatabase: datetime
SybaseASADatabase: timestamp

date
MySQLDatabase: date
SQLiteDatabase: date
H2Database: date
PostgresDatabase: date
UnsupportedDatabase: date
DB2Database: date
MSSQLDatabase: [date]
OracleDatabase: date
HsqlDatabase: date
FirebirdDatabase: date
DerbyDatabase: date
InformixDatabase: date
SybaseDatabase: date
SybaseASADatabase: date

char
MySQLDatabase: CHAR
SQLiteDatabase: CHAR
H2Database: CHAR
PostgresDatabase: CHAR
UnsupportedDatabase: CHAR
DB2Database: CHAR
MSSQLDatabase: char
OracleDatabase: CHAR
HsqlDatabase: CHAR
FirebirdDatabase: CHAR
DerbyDatabase: CHAR
InformixDatabase: CHAR
SybaseDatabase: CHAR
SybaseASADatabase: CHAR

varchar
MySQLDatabase: VARCHAR
SQLiteDatabase: VARCHAR
H2Database: VARCHAR
PostgresDatabase: VARCHAR
UnsupportedDatabase: VARCHAR
DB2Database: VARCHAR
MSSQLDatabase: varchar
OracleDatabase: VARCHAR2
HsqlDatabase: VARCHAR
FirebirdDatabase: VARCHAR
DerbyDatabase: VARCHAR
InformixDatabase: VARCHAR
SybaseDatabase: VARCHAR
SybaseASADatabase: VARCHAR

nchar
MySQLDatabase: NCHAR
SQLiteDatabase: NCHAR
H2Database: NCHAR
PostgresDatabase: NCHAR
UnsupportedDatabase: NCHAR
DB2Database: NCHAR
MSSQLDatabase: nchar
OracleDatabase: NCHAR
HsqlDatabase: CHAR
FirebirdDatabase: NCHAR
DerbyDatabase: NCHAR
InformixDatabase: NCHAR
SybaseDatabase: NCHAR
SybaseASADatabase: NCHAR

nvarchar
MySQLDatabase: NVARCHAR
SQLiteDatabase: NVARCHAR
H2Database: NVARCHAR
PostgresDatabase: VARCHAR
UnsupportedDatabase: NVARCHAR
DB2Database: NVARCHAR
MSSQLDatabase: nvarchar
OracleDatabase: NVARCHAR2
HsqlDatabase: VARCHAR
FirebirdDatabase: NVARCHAR
DerbyDatabase: VARCHAR
InformixDatabase: NVARCHAR
SybaseDatabase: NVARCHAR
SybaseASADatabase: NVARCHAR

clob
MySQLDatabase: LONGTEXT
SQLiteDatabase: TEXT
H2Database: CLOB
PostgresDatabase: TEXT
UnsupportedDatabase: CLOB
DB2Database: CLOB
MSSQLDatabase: varchar
OracleDatabase: CLOB
HsqlDatabase: CLOB
FirebirdDatabase: BLOB SUB_TYPE TEXT
DerbyDatabase: CLOB
InformixDatabase: CLOB
SybaseDatabase: TEXT
SybaseASADatabase: LONG VARCHAR

currency
MySQLDatabase: DECIMAL
SQLiteDatabase: REAL
H2Database: DECIMAL
PostgresDatabase: DECIMAL
UnsupportedDatabase: DECIMAL
DB2Database: DECIMAL(19, 4)
MSSQLDatabase: [money]
OracleDatabase: NUMBER(15, 2)
HsqlDatabase: DECIMAL
FirebirdDatabase: DECIMAL(18, 4)
DerbyDatabase: DECIMAL
InformixDatabase: MONEY
SybaseDatabase: MONEY
SybaseASADatabase: MONEY

uuid
MySQLDatabase: char(36)
SQLiteDatabase: TEXT
H2Database: UUID
PostgresDatabase: UUID
UnsupportedDatabase: char(36)
DB2Database: char(36)
MSSQLDatabase: [uniqueidentifier]
OracleDatabase: RAW(16)
HsqlDatabase: char(36)
FirebirdDatabase: char(36)
DerbyDatabase: char(36)
InformixDatabase: char(36)
SybaseDatabase: UNIQUEIDENTIFIER
SybaseASADatabase: UNIQUEIDENTIFIER

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

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

После запуска spring-boot приложения у нас будет создано 3 таблицы, одна из которых и будет person.

А теперь попробуем добавить новую колонку в таблицу в этом 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)"/>
        <column name="telegram_id" type="int">
            <constraints unique="true"/>
        </column>
        <column name="address" type="varchar(300)"/>
    </createTable>
</changeSet>

Снова запустив приложение мы получим ошибку.

Ошибка при изменении 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)"/>
        <column name="telegram_id" type="int">
            <constraints unique="true"/>
        </column>
    </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.name as person_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 на своей стажировке в 2017. С тех пор я использую Liquibase на своих домашних проектах, и продвигаю его использование на рабочих.

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

Добавляя все changeSet в один changelog у вас могло закрасться сомнение: а все ли мы правильно делаем. Схема БД может довольно динамично меняться, особенно в начале создания приложения, поэтому мы ожидаем множество changeSet.

Можно создавать множество ChangeLog и включать их друг в друга. Далее я расскажу о своем подходе к организации changelog.

Организация хранения changeSet
Вот так это выглядит в проекте

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

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

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

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

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

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

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

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

Все это очень хорошо до тех пор, пока вам не захочется иметь:

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

Разработчики стараются давать понятные имена для переменных, но дополнительное описание не будет лишним. Параметр 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, а так же простое внедрение его в ваше приложение. Этого уже достаточно, чтобы начать пользоваться Liquibase.

В следующей статье я расскажу, как откатывать изменения.

Комментарии

Struchkov Mark
Struchkov Mark
Используете систему управления миграций на своем проекте? Возможно это пустая трата ресурсов и времени? Или быть может дадите свою рекомендацию по работе с Liquibase?