Введение
Liquibase представляет из себя систему управления версиями базы данных, в основном это касается структуры и в меньшей степени содержимого базы. При этом описание базы с одной стороны достаточно абстрактно и позволяет использовать на нижнем уровне различные СУБД, и с другой стороны всегда можно перейти на SQL-диалект конкретной СУБД, что достаточно гибко. Liquibase является устоявшимся проектом с открытым исходным кодом и активно используется за пределами своей родной Java среды и не требует глубоких знаний Java для работы. В качестве описания структуры базы и изменений базы исторически использовался XML формат, однако сейчас параллельно поддерживается YAML и JSON.
В данной статье мы немного обобщим опыт предыдущих поколений и сосредоточимся на работе с Liquibase с использованием Maven. В качестве тестовой операционной системы будем использовать Ubuntu.
Другие статьи о Liquibase
- https://habr.com/ru/post/179425/
- https://habr.com/ru/post/178665/
- https://habr.com/ru/post/333762/
- https://habr.com/ru/post/251617/
- https://habr.com/ru/post/251617/
Настройка окружения
Запускать Liquibase можно несколькими способами, однако наиболее удобно использовать Maven или Gradle.
sudo apt install maven
mvn -version
В качестве Makefile здесь выступает 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
Ограничения и проблемы
Работа с бинарными данными в базе
Существуют отпределенные проблемы с выгрузкой, сравнением и применением бинарных данных, в частности проблема с генерацией изменений.
Наследование и общие столбцы
- http://forum.liquibase.org/topic/postgresql-subtable-via-inherits
- https://stackoverflow.com/questions/25840467/liquibase-common-columns
Исходный код
Альтернативные решения
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 в существующий проект
- https://www.liquibase.org/documentation/existing_project.html
- https://www.liquibase.org/documentation/contexts.html
Как работают изменения базы
- https://www.liquibase.org/documentation/changeset.html
- https://www.liquibase.org/documentation/databasechangelog_table.html
Больше о формате изменений
- http://www.liquibase.org/documentation/json_format.html
- https://www.liquibase.org/documentation/changes/sql.html
- https://www.liquibase.org/documentation/changes/sql_file.html
- https://www.liquibase.org/documentation/column.html
Больше про update
Больше о генерации изменений
Больше о кастомном SQL
- http://www.liquibase.org/documentation/modify_sql.html
- https://stackoverflow.com/questions/28240068/create-column-of-type-double-precision-with-liquibase
Обработка типов данных специфичних для конкретной базы
<createTable tableName="t_name">
...
<column name="doubleArray" type="DOUBLE_ARRAY"/>
...
</createTable>
<modifySql dbms="postgresql">
<replace replace="DOUBLE_ARRAY" with="double precision[][]"/>
</modifySql>
Прочее
Комментарии (12)
HSerg
21.01.2019 22:52+1А почему ссылки на en-ветку хабра? Они же все русскоязычные.
ki77roy Автор
22.01.2019 14:29Искал гуглом, получил такие ссылки… есть ли смысл менять?
HSerg
22.01.2019 21:02Обрадовался было новым англоязычным статьям про Liquibase, а там оказались уже ранее прочтённые. Надумаете что-то дополнить, лучше заодно и ссылки поправить.
Забавно, но мне google даже при явном указании en-ветки вообще ничего не находит.ki77roy Автор
23.01.2019 11:37Немного странно, но это оказалось привязано к настроякам языка в аккаунте, то есть для English всегда перекидывает на en, для Russian на ru. Поменял ссылки на ru.
bvn13
22.01.2019 10:12Как оно работает в кейсах обновления пакетов оракла?
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>
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); } } ...
Jedi_Knight
Дефолтный коммент «почему сразу не gradle»
HSerg
Дефолтный ответ «потому что не соответствует теме статьи»
yashanet
а почему сразу gradle? Maven чего то не умеет?
ki77roy Автор
Я постараюсь выложить примеры шаблонных проектов и с Maven и с Gradle несколько позже, если это интересно.