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

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

Логотип Liquibase и pgCodeKeeper


Я надеюсь, что вы уже знакомы с работой Liquibase, поэтому лишь в двух словах опишу как работает система. Для того, чтобы внести изменения в БД, вам необходимо создать файл миграции (changeset), ссылку на который нужно будет указать в файле журнала изменений (changelog), после чего миграция может быть успешно применена к целевой БД. Неоспоримым и очевидным преимуществом подобного подхода является возможность выполнения отката сделанных изменений.

На мой взгляд, у liquibase есть лишь один существенный недостаток — автоматический откат изменений работает лишь только в том случае, если миграция описана в виде файла XML. Этот момент омрачает головы как разработчиков выполняющих изменения в БД, так и администраторов баз данных выполняющих инспекцию изменений. Разметка XML далеко не так хороша для восприятия, в отличие старого и хорошо всем известного SQL. Liquibase, к счастью, позволяет писать миграции и на SQL, только лишь с одним досадным недостатком — автоматический откат теперь не работает и секцию отката нужно писать вручную.

Сегодня мы исправим это недоразумение и научимся создавать файлы миграций для liquibase, во-первых на SQL, во-вторых с секцией отката, в-третьих эти файлы будут формироваться автоматически. Не верите? Сейчас я всё подробно расскажу.

Создавать миграции мы будем следующим образом — у нас будут две базы данных, одна будет условно для разработки, другая боевая (стейдж, тест — выберите по вкусу). Мы будем вносить изменения в девелоперскую БД используя любимый инструмент, потом сравним базы данных, сформируем миграцию, которую и развернем в целевой базе данных (у нас мы её будем считать боевой).

Все команды ниже будут отображены с учетом работы в Linux shell. Для генерации секции файлов миграции нам потребуется pgCodeKeeper, а для формирования секции rollback наличие в системе утилиты sed.

Давайте условимся, что у нас есть две базы данных — dbdev и dbprod, в dbdev мы вносим изменения вручную, а в dbprod изменения попадают с помощью Liquibase.

Я создам в /tmp каталог migration в котором и буду выполнять всю работу, разумеется вы можете использовать тот каталог, который пожелаете. PostgreSQL у меня уже установлен на локальном компьютере, и именно с ним я и буду проводить работу.

Создаём базы данных для работы:

$ mkdir /tmp/migration
$ cd /tmp/migration/
$ createdb dbdev
$ createdb dbprod

Загружаем драйвера JDBC для PostgreSQL, дистрибутивы Liquibase и pgCodeKeeper

$ wget https://jdbc.postgresql.org/download/postgresql-42.1.3.jar
$ wget https://github.com/liquibase/liquibase/releases/download/liquibase-parent-3.4.2/liquibase-3.4.2-bin.tar.gz
$ wget http://pgcodekeeper.ru/cli/release/pgCodeKeeper-cli-3.11.4.201707170702.zip

Разархивируем liquibase и pgcodekeeper в текущий каталог, вы конечно же распаковать их в выделенные каталоги.

Внимание! С момента написания статьи произошли некоторые изменения. CLI версию pgCodeKeeper можно скачать здесь: github.com/pgcodekeeper/pgcodekeeper/releases

$ tar xzvf liquibase-3.4.2-bin.tar.gz
$ unzip pgCodeKeeper-cli-3.11.4.201707170702.zip

В текущем каталоге создайте файл настроек для liquibase — liquibase.properties со следующим содержимым:

driver: org.postgresql.Driver
classpath: ./postgresql-42.1.3.jar
url: jdbc:postgresql:dbprod
username: user
password: topsecret
changeLogFile: db.changelog.xml

Обращаю внимание, что это настройки для Liquibase, т.е. описывайте в нём соединение с БД dbprod, на которую мы будем развертывать наши миграции. Имя пользователя и пароль установите в свои значения, у меня имя пользователя — ags и в дальнейших сообщения вывода команд будет встречаться как раз это имя. В текущем каталоге создаем файл db.changelog.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-3.1.xsd">
 
</databaseChangeLog>

Проверяем, готов ли liquibase к работе:

$ ./liquibase status
ags@jdbc:postgresql:dbprod is up to date
Liquibase 'status' Successful

Отлично, половина дела сделано, осталось совсем малость. Проверяем, работает ли pgCodeKeeper.

$ ./pgcodekeeper-cli.sh jdbc:postgresql:dbdev jdbc:postgresql:dbprod

если команда не выдала никакого сообщения — значит всё хорошо. Обратите внимание, если ваш инстанс постгреса настроен на другом хосте или порту или используется доступ по паролю, то вы должны сформировать соот��етствующую JDBC URL для вашей базы данных. Как правильно его сформировать читаем здесь.

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

Создадим таблицу в БД dbdev:

[ags@saushkin-ag:/tmp/migration] $ psql dbdev
psql (9.6.3, сервер 9.5.7)
Введите "help", чтобы получить справку.

(ags@[local]:5432) 16:08:43 [dbdev]  =# create table users (id serial primary key, name text);                                                        
CREATE TABLE
Время: 20,708 мс
(ags@[local]:5432) 16:09:16 [dbdev] * =# commit;
COMMIT
Время: 6,913 мс

Проверяем, может ли pgCodeKeeper найти отличия.

$ ./pgcodekeeper-cli.sh jdbc:postgresql:dbdev jdbc:postgresql:dbprod
CREATE SEQUENCE users_id_seq
        START WITH 1
        INCREMENT BY 1
        NO MAXVALUE
        NO MINVALUE
        CACHE 1;

ALTER SEQUENCE users_id_seq OWNER TO ags;

CREATE TABLE users (
        id integer DEFAULT nextval('users_id_seq'::regclass) NOT NULL,
        name text
);

ALTER TABLE users OWNER TO ags;

ALTER TABLE users
        ADD CONSTRAINT users_pkey PRIMARY KEY (id);

ALTER SEQUENCE users_id_seq
        OWNED BY users.id;


Да, всё работает так как и ожидалось. Подготавливаем скрипт создания миграций migrate.sh

#!/bin/bash

FILENAME=${1:-changeset.sql}

# Заголовок файла миграции
echo "--liquibase formatted sql" > $FILENAME
echo "--changeset $USER:$FILENAME" >> $FILENAME
echo "" >> $FILENAME

# Создаем секцию наката
./pgcodekeeper-cli.sh jdbc:postgresql:dbdev jdbc:postgresql:dbprod >> $FILENAME

echo "" >> $FILENAME

# Создаем секцию отката (базы данных идут в обратном порядке)
./pgcodekeeper-cli.sh jdbc:postgresql:dbprod jdbc:postgresql:dbdev | sed -e 's/^/--rollback /' >> $FILENAME

Скрипт принимает один параметр — имя выходного файла, если имя файла не задано, то по умолчанию принимается changeset.sql. Далее формируется заголовок файла SQL, который необходим Liquibase для сохранения информации о выполненных изменениях на целевой БД. Далее идёт формирование собственно файла миграции.

Команда:

./pgcodekeeper-cli.sh jdbc:postgresql:dbdev jdbc:postgresql:dbprod >> $FILENAME

находит различия между базами, формирует файл преобразования структуры объектов БД из dbdev в dbprod и сохраняет в выходном файле.

А команда:

./pgcodekeeper-cli.sh jdbc:postgresql:dbprod jdbc:postgresql:dbdev | sed -e 's/^/--rollback /' >> $FILENAME

ищет различия между этими же базами только в другом направлении, целевой БД теперь становится девелоперская, для того, чтобы можно было выполнить автоматический откат изменений. Секция отката в Liquibase помечается комментариями --rollback.

Проверяем, формирование миграции. Для теста, в качестве выходного файла, я использую /dev/stdout:

$ chmod +x ./migrate.sh
$ ./migrate.sh /dev/stdout 
--liquibase formatted sql
--changeset ags:/dev/stdout

CREATE SEQUENCE users_id_seq
        START WITH 1
        INCREMENT BY 1
        NO MAXVALUE
        NO MINVALUE
        CACHE 1;

ALTER SEQUENCE users_id_seq OWNER TO ags;

CREATE TABLE users (
        id integer DEFAULT nextval('users_id_seq'::regclass) NOT NULL,
        name text
);

ALTER TABLE users OWNER TO ags;

ALTER TABLE users
        ADD CONSTRAINT users_pkey PRIMARY KEY (id);

ALTER SEQUENCE users_id_seq
        OWNED BY users.id;


--rollback ALTER TABLE users
--rollback      DROP CONSTRAINT users_pkey;
--rollback 
--rollback DROP TABLE users;
--rollback 

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

Формируем реальный файл миграции

$ ./migrate.sh 001_users.sql

Подключаем его к журналу миграций, отредактировав файл db.changelog.xml, добавив в него директиву include с файлом 001_users.sql:

<?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-3.1.xsd">
 
    <include file="001_users.sql" />
 
</databaseChangeLog>


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

$ ./liquibase tag 001_before_users
Successfully tagged ags@jdbc:postgresql:dbprod
Liquibase 'tag' Successful

$ ./liquibase migrate
Liquibase Update Successful

Проверяем статус

$ ./liquibase status
ags@jdbc:postgresql:dbprod is up to date
Liquibase 'status' Successful

Откатываемся на состояние перед накатом таблицы

$ ./liquibase rollback 001_before_users
Liquibase Rollback Successful

Проверяем статус повторно

$ ./liquibase status
1 change sets have not been applied to ags@jdbc:postgresql:dbprod
Liquibase 'status' Successful

Возвращаем выполненный предварительно накат

$ ./liquibase migrate
Liquibase Update Successful

Давайте проверим, отличаются ли структуры баз данных сейчас? Теперь, после того как мы «поигрались» с накатами и откатами состояние баз данных dbdev и dbprod должно быть одинаковым. Так ли это?

$ ./pgcodekeeper-cli.sh jdbc:postgresql:dbdev jdbc:postgresql:dbprod
ALTER TABLE databasechangeloglock
        DROP CONSTRAINT pk_databasechangeloglock;

DROP TABLE databasechangeloglock;

DROP TABLE databasechangelog;

pgCodeKeeper говорит, что в dbdev нет двух таблиц — databasechangeloglock и databasechangelog. Эти таблицы были созданы автоматически liquibase и в них он сохраняет информацию о выполненных накатах. Без этих таблиц работа liquibase будет невозможна. Чтобы эти таблицы в дальнейшем не мешали нам при формировании миграций можно или скопировать структуру этих таблиц в девелоперскую базу или использовать возможность списка игнорирования для pgCodeKeeper.

Для того, чтобы задействовать возможность по игнорированию объектов создайте файл .pgcodekeeperignore в текущем каталоге со следующим содержимым:

$ cat .pgcodekeeperignore 
SHOW ALL
HIDE REGEX "databasechangelog.*"

Теперь, команда

$ ./pgcodekeeper-cli.sh -I .pgcodekeeperignore jdbc:postgresql:dbdev jdbc:postgresql:dbprod

не должна отображать никаких изменений.

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

  1. Вносим изменения в девелоперскую БД
  2. С помощью нашего скрипта формируем миграцию
  3. Инспектируем миграцию и подключаем её в журнал миграций
  4. Деплоим изменения на боевую (тест, стейдж) БД
  5. В случае необходимости, делаем откат

Создание файлов миграций Liquibase теперь стало возможно выполнять автоматически. Миграции описаны на языке SQL и что немаловажно, секция отката создается также без участия человека. Самое сложное что нужно сделать, это придумать имя файла миграции.

P.S.: У Liquibase есть встроенный механизм формирования чейнджсета различий между двумя БД, к сожалению, по своему опыту могу сказать, что он работает не всегда хорошо, заменив этот механизм на код формируемый pgCodeKeeper можно формировать миграции для более сложных изменений в БД.

P.P.S.: Со времени написания статьи, размещение релизов CLI изменилось, теперь они находятся здесь: github.com/pgcodekeeper/pgcodekeeper/releases

Сайт обновлений для Eclipse находится здесь: pgcodekeeper.org/update