О применении Liquibase в проектах разработки

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

Определение Liquibase: «Liquibase is an open‑source database‑independent library for tracking, managing and applying database schema changes», представление продукта на официальном сайте: «Accelerate Your Database Deployments», «CI/CD for the database», «Release software faster by bringing DevOps to the database with Liquibase».

Определение Flyway: «Flyway is an open‑source database‑migration tool», представление продукта на официальном сайте: «Increase reliability of deployments by versioning your database», «Database migrations made easy», «Made for CI/CD».

По всей видимости, первичное восприятие у большинства формируется такое: это инструменты для DevOps. Дополняет такое восприятие декларируемое предназначение: «Миграции баз данных», которое, как я считаю, неудачным образом распространяется дальше в тематических статьях как основное предназначение этих инструментов. Т.е. понимание сводится к тому, что если речь заходит о применении указанных инструментов, то предполагается наличие уже существующей, разработанной/разрабатываемой, базы данных, изменения из которой теперь будут замечательным образом переноситься с одного стенда на другой, разово или на регулярной основе.

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

Попробуем разобраться, собственно, в чем исходная проблема, почему вообще появились такие инструменты, для чего вообще нужно в них разбираться, и почему «It is what it is».

Как известно, нельзя просто так взять и пересоздать базу данных, находящуюся в эксплуатации (в т.ч. ранее развернутую в общей тестовой среде с множеством разработчиков, работающих над одним проектом). Выдвигая это утверждение, для осторожности, ограничусь только реляционными базами данных, и уточню, что речь идет о структуре БД — DDL-инструкциях для таблиц, в общем случае.

94ac15e1d94c2fceb3226395d1935a17.jpg

В «обычном» приложении структурные элементы проекта можно дополнить/изменить, после чего проект пересобирается и разворачивается на стенде, с замещением ранее развернутой копии приложения новой его версией.

// Версия 0.1

// /incredible-app/src/main/java/net/cuttingedge/SilverBulletSolution.java

package net.cuttingedge;

public class SilverBulletSolution {
    int magicAttribute;

    public static void main(String[] args) {
        System.out.println("This is the initial version!");
    }
}
// Версия 0.2

// /incredible-app/src/main/java/net/cuttingedge/SilverBulletSolution.java

package net.cuttingedge;

public class SilverBulletSolution {
    int magicAttribute;
    String yetAnotherMagicAttribute;
    
    public static void main(String[] args) {
        System.out.println("This is a breakthrough improvement!");
    }
}

В случае с базой данных сделать также — не получится: дополненное/измененное определение главного структурного элемента проекта — таблицы, — просто так не замещается, приходится добавлять отдельные инструкции по изменению.

-- Версия 0.1

-- /incredible-app/src/main/database/cuttingedge/silver_bullet_solution.sql

create schema cuttingedge;

create table cuttingedge.silver_bullet_solution(
    magic_attribute integer       
);
-- Версия 0.2

-- /incredible-app/src/main/database/cuttingedge/01-silver_bullet_solution.sql

create schema if not exists cuttingedge;

create table if not exists cuttingedge.silver_bullet_solution(
    magic_attribute integer       
);

-- /incredible-app/src/main/database/cuttingedge/02-silver_bullet_solution-new_attribute.sql

alter table cuttingedge.silver_bullet_solution
    add column yet_another_magic_attribute text
;

Хорошо, что есть, то есть.

Проект базы данных ведь может быть представлен в виде текстовых файлов — скриптов, разложенных по папкам и выстроенных в нужной последовательности, а значит, укладывается в репозиторий системы управления версиями (VCS) и может быть, в итоге, развернут из нужной ветки на нужный стенд в нужном состоянии. По этому пути и идут. Например, выстраивают систему сборки целевой БД с использованием Shell-скриптов и тех или иных дополнительных инструментов. Разработчики при этом разрабатывают скрипты отдельно, задействуя какой-нибудь удобный менеджер БД (database tool) и используя подготовленный стенд с развернутой СУБД, локальный и/или разделяемый, после чего составляют Pull/Merge Request и направляют его на Code Review. Итоговая консолидация проекта, после слияния изменений, автоматизированная в той или иной степени, дает конечный результат. Схема работы понятна и особых вопросов не вызывает. Все нормально, если все делается так, как задумано. Обнаруживаемые при этом, ранее неиспользованные, инструменты, такие как Liquibase и Flyway, воспринимаются часто как «yet another tool», которые, вероятно, более удобные и на которые стоит переходить, т.к. они получают статус стандарта де-факто для решения подобных задач.

И к своему удивлению, я неоднократно слышал от разработчиков, намеревающихся использовать Liquibase, примерно следующее: «Я буду продолжать разрабатывать свои скрипты так, как и раньше, в рамках понятной мне проектной модели. А для Liquibase нужно, чтобы скрипт развертывания как-то был сгенерирован в нужном формате, и, далее, выполнен стороной DevOps». В моем понимании, это равносильно такому высказыванию: «У меня есть папка программного проекта с текстовыми файлами. Нужно чтобы кто-то (не я, например, DevOps) произвел сборку проекта». Наверное, можно возразить, что разработчик же всегда будет направлять на развертывание корректные изменения. Но, в моем понимании, проблема сложности реальных enterprise-проектов находится на таком уровне, что не стоит полагаться на то, что ты помнишь или не помнишь, что и где ты сделал или не сделал. Уверенность в корректности производимых изменений строится на успешности их применения на текущем стенде? А стенд в каком состоянии? А какой это стенд? А когда и кто на этом или другом стенде производил развертывание, и какое? Наверное, гораздо лучше полагаться на автомат, который обеспечивает контроль корректности, часто неочевидный. И правильный путь — это полная сборка проекта целевой БД при каждом внесении в проект изменений, включающая развертывание на доступном стенде, т.е. когда этап развертывания неотделим от процесса разработки (развертывание репетируется) — само вырабатываемое разработчиком проектное решение включает развертывание.

Liquibase может и, на мой взгляд, должен использоваться как инструмент организации и сборки проекта разработки базы данных — это его главное предназначение. Структура проекта Liquibase изначально должна закладываться, как древовидная иерархия файлов формата Changelog. Генерация Changelog-файла из существующей БД применяется только на начальном этапе для старых проектов, которые переводятся в формат Liquibase, или если необходима какая-то разовая миграция. Проектный каталог, также, как и обычно, сохраняется в репозитории системы управления версиями (VCS). А под версионированием со стороны Liquibase понимается автоматически обеспечиваемый процесс применения тех изменений в рамках текущего обновления, которые необходимы к применению на конкретном стенде (в целевой БД Liquibase регистрирует примененные изменения в специальных служебных таблицах (Tracking Tables), что позволяет ему при следующем обновлении определить, какие изменения уже были применены, а какие — еще нет). При этом, нет строгой необходимости в применении отдельных веток в рамках VCS для разных стендов — одно и то же состояние проекта можно выносить на разные стенды, с различным текущим состоянием, причем неоднократно — вопрос разделения состояний проекта будет определяться только необходимостью разделения стадий для доработок. Прийти к желаемой модели замещения доработанной таблицы на основе ее локализованного в одном блоке кода определении, Liquibase, конечно же, не позволит — по прошествии времени, определение целевой таблицы все равно будет представлено в проекте как начальная инструкция create и серия последующих инструкций alter (есть возможность, по истечении некоторого времени, после обновления всех задействованных в процессе стендов, «причесать» определение таблицы, объединив все дополнения к таблице в единой инструкции create, но это не совсем тривиальная процедура). Тем не менее, такой инструментарий и так позволяет заметно «приподнять» проектную модель для БД на пути к высокоуровневой разработке, основанной на принципах Agile с CI/CD.

Еще больше приблизиться к желаемой высокоуровневой проектной модели для БД позволяет подход, основанный на применении метаданных. Он позволяет перейти на декларативное описание структуры БД, с привнесением в проект объектно-ориентированной парадигмы (частично). В результате, инструкции Liquibase будут сведены к простым инструкциям по внесению изменений в таблицы метаданных; проект по разработке целевой БД, в части структуры, превратится в набор деклараций (это не касается кода целевых представлений, функций и процедур, для которых обобщенные элементы могут быть применены лишь частично). Конечно, все это обеспечивается специальной платформой, которая должна быть разработана, если ее нет. Но главная мотивирующая идея такого подхода — это обеспечение возможности концентрации над логическим проектным решением для прикладного разработчика, и переключение от работы над экземпляром целевой системы к работе над генератором экземпляров — для системного разработчика. И это материал для отдельной статьи.

© Habrahabr.ru