Введение


Liquibase представляет из себя систему управления версиями базы данных, в основном это касается структуры и в меньшей степени содержимого базы. При этом описание базы с одной стороны достаточно абстрактно и позволяет использовать на нижнем уровне различные СУБД, и с другой стороны всегда можно перейти на SQL-диалект конкретной СУБД, что достаточно гибко. Liquibase является устоявшимся проектом с открытым исходным кодом и активно используется за пределами своей родной Java среды и не требует глубоких знаний Java для работы. В качестве описания структуры базы и изменений базы исторически использовался XML формат, однако сейчас параллельно поддерживается YAML и JSON.


В данной статье мы немного обобщим опыт предыдущих поколений и сосредоточимся на работе с Liquibase с использованием Maven. В качестве тестовой операционной системы будем использовать Ubuntu.


Другие статьи о Liquibase



Настройка окружения


Запускать Liquibase можно несколькими способами, однако наиболее удобно использовать Maven или Gradle.


sudo apt install maven
mvn -version

В качестве Makefile здесь выступает pom.xml — он уже содержит все необходимые зависимости, настройки и профили.


pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.test.db</groupId>
    <artifactId>db</artifactId>
    <version>1.0.0</version>
    <name>db</name>
    <description>Test Database</description>
    <packaging>pom</packaging>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <slf4j.version>1.7.24</slf4j.version>
        <logback.version>1.2.3</logback.version>
        <liquibase.version>3.6.2</liquibase.version>
        <postgresql.version>42.2.5</postgresql.version>
        <snakeyaml.version>1.23</snakeyaml.version>
    </properties>
    <dependencies>
        <!--Logging-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>log4j-over-slf4j</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback.version}</version>
        </dependency>

        <!--JDBC drivers-->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>${postgresql.version}</version>
        </dependency>
        <dependency>
            <groupId>org.liquibase</groupId>
            <artifactId>liquibase-core</artifactId>
            <version>${liquibase.version}</version>
        </dependency>
        <dependency>
            <groupId>org.yaml</groupId>
            <artifactId>snakeyaml</artifactId>
            <version>${snakeyaml.version}</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.liquibase</groupId>
                <artifactId>liquibase-maven-plugin</artifactId>
                <version>${liquibase.version}</version>
                <configuration>
                    <propertyFile>${profile.propertyFile}</propertyFile>
                    <changeLogFile>${profile.changeLogFile}</changeLogFile>
                    <dataDir>${profile.dataDir}</dataDir>
                    <!-- log  -->
                    <verbose>${profile.verbose}</verbose>
                    <logging>${profile.logging}</logging>
                    <promptOnNonLocalDatabase>false</promptOnNonLocalDatabase>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <profiles>
      <!-- Development settings, -Denv=dev -->
      <profile>
        <id>dev</id>
        <activation>
          <property>
            <name>env</name>
            <value>dev</value>
          </property>
        </activation>
        <properties>
          <profile.propertyFile>dev/liquibase.properties</profile.propertyFile>
          <profile.changeLogFile>dev/master.xml</profile.changeLogFile>
          <profile.dataDir>dev/data</profile.dataDir>
          <profile.verbose>true</profile.verbose>
          <profile.logging>debug</profile.logging>
        </properties>
      </profile>
      <!-- Production settings, -Denv=prod -->
      <profile>
        <id>prod</id>
        <activation>
          <property>
            <name>env</name>
            <value>prod</value>
          </property>
        </activation>
        <properties>
          <profile.propertyFile>prod/liquibase.properties</profile.propertyFile>
          <profile.changeLogFile>prod/master.xml</profile.changeLogFile>
          <profile.dataDir>prod/data</profile.dataDir>
          <profile.verbose>false</profile.verbose>
          <profile.logging>info</profile.logging>
        </properties>
      </profile>
    </profiles>
</project>

Запускаем обновление


После того как мы сделали pom.xml можно запускать обновление базы — команда liquibase:update.


Для этого нам потребуются:


  • liquibase.properties файл с настройками соединения к базе (логин/пароль и возможно другие параметры)
  • xml файл с изменениями базы
  • sh скрипт запуска обновления базы

Файл с настройками соединения к базе


liquibase.properties


username=test
password=test
referenceUsername=test
#можно задавать и другие параметры
#url=jdbc:postgresql://dev/test
#referenceUrl=jdbc:postgresql://dev/test_reference

Файл с изменениями базы


Основным понятием liquibase являются так называемые изменения базы (changesets). Они могут включать в себя как изменения структуры так и изменение данных. Для контроля примененных изменений liquibase использует таблицы databasechangelog и databasechangeloglock.


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

    <changeSet context="legacy" author="author (generated)" id="1">
        <createTable tableName="test">
            <column autoIncrement="true" name="id" type="SERIAL">
                <constraints nullable="false"/>
            </column>
            <column name="user_name" type="VARCHAR(255)"/>
            <column name="preferences" type="TEXT"/>
        </createTable>
    </changeSet>
</databaseChangeLog>

Скрипт запуска обновления базы


Здесь выполняется liquibase:update для профиля dev и базы из liquibase.url, которая указана в стандартном JDBC формате. После обновления в базе появляется указанная в changeSet таблица и две служебные таблицы databasechangelog и databasechangeloglock.


#!/usr/bin/env bash
mvn liquibase:update    -Denv=dev    -Dliquibase.url="jdbc:postgresql://dev/test?prepareThreshold=0&stringtype=unspecified"

Генерация SQL без обновления базы


Иногда перед запуском изменений требуется посмотреть содержимое создаваемых запросов. Для этого предназначены команды liquibase:updateSQL и liquibase:rollbackSQL.


#!/usr/bin/env bash
mvn liquibase:updateSQL    -Denv=dev    -Dliquibase.url="jdbc:postgresql://dev/test?prepareThreshold=0&stringtype=unspecified" > /tmp/script.sql

Подробнее о changeSet


Изменения могут быть в разных форматах, в том числе обычный sql или он же в отдельном файле.


Каждое изменение может включать секцие rollback позволяющую откатывать изменения командой liquibase:rollback. Кроме того для маркировки изменений, например для более удобного отката туда, можно использовать tagDatabase.


Обычный формат


<changeSet context="legacy" author="author (generated)" id="1">
    <createTable tableName="test">
        <column autoIncrement="true" name="id" type="SERIAL">
            <constraints primaryKey="true" primaryKeyName="test_pkey"/>
        </column>
        <column name="c1" type="VARCHAR(255)"/>
        <column name="c2" type="INTEGER"/>
        <column name="c3" type="SMALLINT"/>
        <column name="c4" type="VARCHAR(255)"/>
        <column name="c5" type="TEXT"/>
        <column name="c6" type="VARCHAR(255)"/>
    </createTable>
</changeSet>

Встроенный SQL


<changeSet context="legacy" author="author" id="1-domain-some-domain">
    <sql>
        CREATE DOMAIN public.some_domain AS bigint;
        ALTER DOMAIN public.some_domain OWNER TO test;
    </sql>
    <rollback>
        DROP DOMAIN public.some_domain;
    </rollback>
</changeSet>

Файл SQL


<changeSet context="legacy" author="author" id="1-user">
     <sqlFile dbms="postgresql" path="sql/some.sql" relativeToChangelogFile="true" />
     <rollback> delete from "some"; </rollback>
</changeSet>

Тэги


<changeSet context="legacy" author="author" id="1-initial-changeset">
    <tagDatabase tag="initial"/>
</changeSet>

Контексты запуска


Для более удобного управления различными конфигурациями, например development/production можно использовать контексты. Контекст указывается в changeSet аттрибуте context и затем запускается Maven параметром -Dcontexts.


Изменение с контекстом


<changeSet context="legacy" author="author" id="1-initial-changeset">
    <tagDatabase tag="initial"/>
</changeSet>

Запуск изменений по контексту


#!/usr/bin/env bash
mvn liquibase:update    -Denv=dev    -Dliquibase.url="jdbc:postgresql://dev/test?prepareThreshold=0&stringtype=unspecified"    -Dliquibase.contexts=non-legacy

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


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


Изменение с откатом


<changeSet context="legacy" author="author" id="1-domain-some-domain">
    <sql>
        CREATE DOMAIN public.some_domain AS bigint;
        ALTER DOMAIN public.some_domain OWNER TO test;
    </sql>
    <rollback>
        DROP DOMAIN public.some_domain;
    </rollback>
</changeSet>

Запуск отката


#!/usr/bin/env bash
mvn liquibase:update    -Denv=dev    -Dliquibase.url="jdbc:postgresql://dev/test?prepareThreshold=0&stringtype=unspecified"    -Dliquibase.contexts=non-legacy

Сравнение


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


liquibase.properties


referenceUsername=test
referenceUrl=jdbc:postgresql://dev/test_reference

Сравнение схем


Сравнение схем url и referenceUrl.


#!/usr/bin/env bash
mvn liquibase:diff    -Denv=dev    -Dliquibase.referenceUrl="jdbc:postgresql://dev/test?prepareThreshold=0"    -Dliquibase.url="jdbc:postgresql://dev/test_reference?prepareThreshold=0"    -Dliquibase.diffChangeLogFile=dev/diff.xml

Сохранение схемы


Также бывает полезно сохранить текущую схему базы, с данными или без. Необходимо иметь ввиду, что Liquibase сохраняет схему не полностью соответствующую оригиналу, например используемые домены или наследование нужно будет добавлять отдельно (см Ограничения).


Сохранение схемы без учёта данных


Сохранение схемы существующей базы.


#!/usr/bin/env bash
mvn liquibase:generateChangeLog    -Denv=dev    -Dliquibase.url="jdbc:postgresql://dev/test_reference?prepareThreshold=0"    -Dliquibase.outputChangeLogFile=dev/changelog.xml

Сохранение схемы с данными


Сохранение схемы существующей базы с данными.


#!/usr/bin/env bash
mvn liquibase:generateChangeLog    -Denv=dev    -Dliquibase.url="jdbc:postgresql://dev/test_reference?prepareThreshold=0"    -Dliquibase.outputChangeLogFile=dev/changelog.xml

Ограничения и проблемы


Работа с бинарными данными в базе


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



Наследование и общие столбцы



Исходный код



Альтернативные решения


Flyway


Наряду с Liquibase пользуется популярностью в Java сообществе — http://flywaydb.org/documentation


Sqitch


Аналог на Perl — http://sqitch.org


FluentMigrator


Аналог для .Net — https://github.com/schambers/fluentmigrator


DBGeni


Аналог для Ruby — http://dbgeni.appsintheopen.com/manual.html


Приложения


Структура проекта


pom.xml - maven makefile
  dev
    liquibase.properties - login/password etc
    master.xml - changesets

Как добавить liquibase в существующий проект



Как работают изменения базы



Больше о формате изменений



Больше про update



Больше о генерации изменений



Больше о кастомном SQL



Обработка типов данных специфичних для конкретной базы


<createTable tableName="t_name">
...
<column name="doubleArray" type="DOUBLE_ARRAY"/>
...
</createTable>
<modifySql dbms="postgresql">
<replace replace="DOUBLE_ARRAY" with="double precision[][]"/>
</modifySql>

Прочее


Комментарии (12)


  1. Jedi_Knight
    21.01.2019 20:18

    Дефолтный коммент «почему сразу не gradle»


    1. HSerg
      21.01.2019 22:49

      Дефолтный ответ «потому что не соответствует теме статьи»


    1. yashanet
      22.01.2019 14:28
      +1

      а почему сразу gradle? Maven чего то не умеет?


    1. ki77roy Автор
      22.01.2019 14:34

      Я постараюсь выложить примеры шаблонных проектов и с Maven и с Gradle несколько позже, если это интересно.


  1. HSerg
    21.01.2019 22:52
    +1

    А почему ссылки на en-ветку хабра? Они же все русскоязычные.


    1. ki77roy Автор
      22.01.2019 14:29

      Искал гуглом, получил такие ссылки… есть ли смысл менять?


      1. HSerg
        22.01.2019 21:02

        Обрадовался было новым англоязычным статьям про Liquibase, а там оказались уже ранее прочтённые. Надумаете что-то дополнить, лучше заодно и ссылки поправить.

        Забавно, но мне google даже при явном указании en-ветки вообще ничего не находит.


        1. ki77roy Автор
          23.01.2019 11:37

          Немного странно, но это оказалось привязано к настроякам языка в аккаунте, то есть для English всегда перекидывает на en, для Russian на ru. Поменял ссылки на ru.


  1. bvn13
    22.01.2019 10:12

    Как оно работает в кейсах обновления пакетов оракла?


    1. Ariant
      22.01.2019 14:28
      +1

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


      <changeSet id="package" runOnChange="true">
        <sqlFile path="..\Packages\package.pck" splitStatements="true" endDelimiter="\n/" encoding="Windows-1251"/>
        <rollback/>
      </changeSet>


  1. IvanVakhrushev
    22.01.2019 14:28

    Я в своё время разбирался, как запустить миграции Liquibase из java-кода. Для PostgreSQL, для конкретной схемы.
    Если кому-нибудь интересно, то пример есть здесь.

    Пример кода
    ...
     public static void main(String[] args) {
            try (Connection connection = getConnection()) {
                createSchema(connection, Const.SCHEMA_NAME);
                updateDatabaseStructure(connection);
            } catch (SQLException | ClassNotFoundException e) {
                logger.error(e.getMessage(), e);
            }
        }
    ...
        private static void updateDatabaseStructure(Connection connection) {
            try {
                Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
                database.setDefaultSchemaName(Const.SCHEMA_NAME);
                database.setLiquibaseSchemaName(Const.SCHEMA_NAME);
                final Liquibase liquibase = new Liquibase(Const.LIQUIBASE_CHANGELOG_FILE, new ClassLoaderResourceAccessor(), database);
                liquibase.update(new Contexts(), new LabelExpression());
            } catch (LiquibaseException e) {
                logger.error(e.getMessage(), e);
            }
        }
    ...
    


  1. Vicking
    22.01.2019 23:18

    Букву I верните в заголовок, пожалуйста: Liquibase и Maven