Написать пост

Миграции баз данных с помощью библиотеки Liquibase

Что такое миграции баз данных, когда и на каких проектах их настраивают, как они работают, — рассказываем в материале. А также по шагам разбираем, как настроить миграции с помощью открытой библиотеки Liquibase.

Миграции баз данных — что это такое и для чего нужно

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

Зачем нужны миграции БД и как они упрощают жизнь разработчикам

  1. По мере разработки приложения схема базы данных меняется. Добавляются новые таблицы и столбцы. Миграции позволяют упростить отслеживание этих изменений.
  2. У современных проектов зачастую есть несколько стендов — стенд разработки, тестирования, прод и другие. Возникает проблема синхронизации базы данных — нужно передавать изменения на стенды последовательно и без конфликтов. Миграции помогают решить эту проблему.
  3. Современные проекты работают по методологии Agile, а в ней сложно определить структуру БД на старте. Она развивается вместе с проектом от спринта к спринту. Поэтому автоматизированный рефакторинг БД должен быть таким же обязательным инструментом, как и рефакторинг любых других компонентов.
  4. Поскольку миграции являются частью исходного кода проекта, изменение структуры БД могут быть одобрены или отклонены во время код ревью до того, как они попадут в релизную ветку.

Чаще всего для настройки миграций БД в Java-приложениях используют инструменты Liquibase или Flyway. Они оба написаны на Java, легко интегрируются с maven, gradle и другими инструментами сборки, что способствует большей кастомизации. Предлагают Java API и могут быть расширены. Основное отличие между ними — это форматы, в которых инструмент записывает изменения. Flyway использует только SQL, а Liquibase работает также с XML, YAML или JSON.

Как работают миграции

  • Изменения, которые разработчик вносит в схему базы данных, записываются в текстовых файлах конфигураций, понятных Liquibase или Flyway. Эти изменения преобразуются в SQL-запросы, с которыми инструмент обращается к БД и вносит необходимые изменения.
  • Все изменения, которые мы вносим, хранятся в отдельных файлах. Часто их называют чейнджлогами.
  • В файлах-чейнджлогах изменения представляются в виде чейнджсетов, так называемых точек сохранения. Чейнджсет может хранить одно или несколько изменений базы данных. Каждый чейнджсет уникально идентифицируется.
  • При первом запуске миграции Liquibase или Flyway создает таблицу в схеме базы данных для отслеживания примененных чейнджсетов и дальше работает с ней автоматически. Если изменение уже применялось, повторного выполнения не будет.

Библиотека Liquibase: что это такое и для каких проектов подходит

Liquibase — это открытая независимая от БД библиотека для отслеживания, управления и применения изменений схемы базы данных. Поддерживает подавляющее большинство БД, включая PostgreSQL, MySQL, Oracle, Sybase, HSQL, Apache Derby. Работает с форматами XML, YAML, JSON, SQL.

Ее преимущества: библиотека Liquibase предоставляет больше возможностей из «коробки» в отличие от того же Flyway — отмена изменений, автогенерация миграций. Имеет dry-run, то есть можно посмотреть, какие SQL-запросы будут выполнены.

В отличие от Flyway, которая поддерживает скрипты миграции только в форматах SQL и Java, Liquibase — это универсальный инструмент. Он позволяет накатывать одни и те же миграции на любые базы данных и абстрагироваться от SQL. Эта библиотека больше подходит для проектов, где необходимо работать с разными окружениями и СУБД.

Liquibase не стоит использовать, если ваш проект имеет простую структуру базы данных. А также если вам нужна возможность изменять схему БД с помощью полностью кастомного SQL или с использованием Java-кода. В таком случае удобнее обратиться к Flyway.

Настраиваем миграцию с помощью Liquibase

Как настроить миграцию уже готовой базы данных

  1. Подключаем библиотеку Liquibase, то есть добавить зависимость в вашем проекте.
  2. Пишем нулевой вариант базы данных, то есть описываем скриптами ту БД, которая уже развернута.
  3. Все последующие изменения в структуре БД вносим только через скрипты.

Как создать миграцию для новой базы данных

Ссылка на проект в Bitbucket

Первый шаг. Для работы с библиотекой нам нужно добавить зависимость в файл pom.xml. Ссылка на зависимость тут. Ниже пример, как это сделать.

			‹dependency>
    ‹groupId>org.liquibase‹/groupId>
    ‹artifactId>liquibase-core‹/artifactId>
‹/dependency>
		

Шаг второй. Теперь нужно создать сам скрипт Liquibase. Как уже говорили, для работы можно использовать форматы XML, YAML, JSON или SQL. Мы будем использовать XML как наиболее наглядный. Его преимущества в том, что он позволяет автоматически подставлять названия тегов и атрибутов в файле.

В начале создаем главный запускаемый файл changelog.xml, в который мы будем добавлять пути к скриптам. В changelog.xml вставляем стандартный пустой шаблон по пути: src/main/resources/db/changelog/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 1

Описываем чейнджсет для создания таблицы genre. Для этого создаем файл по пути: src/main/resources/db/changelog/create-changeset-genre-table.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">
   ‹changeSet id="create_table_genre" author="mediaSoft">
      ‹!-- Прописываем создание таблицы genre-->
       ‹createTable tableName="genre">
        ‹!--Создаем поля -->
           ‹column autoIncrement="true" name="genre_id" type="bigint">
               ‹constraints primaryKey="true" nullable="false"/>
           ‹/column>
           ‹column name="genre_name" type="varchar(64)">
               ‹constraints nullable="false" unique="true"/>
           ‹/column>
       ‹/createTable>
   ‹/changeSet>
‹/databaseChangeLog>
		

Напомним, что чейнджсет — это что-то вроде аналога коммита в системах контроля версий. Он может содержать одно или несколько изменений базы данных. Хорошей практикой считается одно изменение для одного чейнджсета — это может быть создание или изменение таблицы, удаление объекта, добавление индекса или данных в таблицу. Каждый чейнджсет обязательно должен иметь два идентификатора: уникальный id и author. Остальные теги можно посмотреть на официальном сайте.

Теперь занесем в таблицу genre данные с помощью тега insert. Для этого создадим новый файл по пути src/main/resources/db/changelog/insert-changeset-genre-table.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">

‹!-- Добавим 4 новых жанра -->

   ‹changeSet id="insert-into-genre" author="mediaSoft">
       ‹insert tableName="genre">
           ‹column name="genre_name" value="Роман"/>
       ‹/insert>
       ‹insert tableName="genre">
           ‹column name="genre_name" value="Поэма"/>
       ‹/insert>
       ‹insert tableName="genre">
           ‹column name="genre_name" value="Рассказ"/>
       ‹/insert>
       ‹insert tableName="genre">
           ‹column name="genre_name" value="Эпос"/>
       ‹/insert>
   ‹/changeSet>

‹/databaseChangeLog>
		

Далее создаем чейнджсет для добавления таблицы book по пути: src/main/resources/db/changelog/create-changeset-book-table.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">
   ‹changeSet id="create_table_book" author="mediaSoft">
       ‹createTable  tableName="book">
           ‹column autoIncrement="true" name="book_id" type="bigint">
               ‹constraints primaryKey="true" nullable="false"/>
           ‹/column>
           ‹column name="title" type="varchar(100)">
               ‹constraints nullable="false"/>
           ‹/column>
           ‹column name="genre_id" type="bigint">
               ‹constraints nullable="false"/>
           ‹/column>
           ‹column name="price" type="double">
               ‹constraints nullable="false"/>
           ‹/column>
           ‹column name="amount" type="integer">
               ‹constraints nullable="false"/>
           ‹/column>
       ‹/createTable>
 ‹/changeSet>
‹/databaseChangeLog>
		

Шаг четвертый. Теперь нам нужно связать наши таблицы. Для этого описываем создание внешнего ключа в changeset-genre-table.xml:

			‹addForeignKeyConstraint baseColumnNames="genre_id"
                        baseTableName="book"
                        constraintName="fk_author_id"
                        referencedColumnNames="genre_id"
                        referencedTableName="genre"/>
		

Шаг пятый. Теперь пропишем скрипт для заполнения таблицы book. Создаем новый файл по пути: src/main/resources/db/changelog/insert-changeset-book-table.xml

Воспользуемся возможностями Liquibase и проверим чейнджсет перед его выполнением на соблюдение условия, есть ли в таблице genre нужный нам жанр.

Для этого мы объявляем тег preConditions. В этом теге указываем результат проверки — он записывается в параметре expectedResult. При проверке мы будем использовать SQL, для этого объявляем тег sqlCheck.

			‹?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">
‹!-- Добавим 1 книгу  -->
   ‹changeSet id="insert-into-book" author="mediaSoft">
         ‹preConditions  onFail="WARN">
   ‹sqlCheck  expectedResult="4">select count(*) from   genre‹/sqlCheck>
‹/preConditions>
‹comment>Проверяем что в нашей таблице жанры есть 4 жанра перед добавлением книги‹/comment>
       ‹insert tableName="book">
           ‹column name="title" value="Капитанская дочка"/>
           ‹column name="genre_id" value="0"/>
           ‹column name="price" value="1200"/>
           ‹column name="amount" value="14"/>
       ‹/insert>
   ‹/changeSet>
‹/databaseChangeLog>
		

Шаг шестой. Теперь в наш корневой файл changelog.xml импортируем наши миграции:

			‹include file="changeset-genre-table.xml" relativeToChangelogFile="true"/>
‹include file="changeset-book-table.xml" relativeToChangelogFile="true"/>
‹include file="insert-changeset-genre-table.xml" relativeToChangelogFile="true"/>
‹include file="insert-changeset-book-table.xml" relativeToChangelogFile="true"/>
		

Важный момент: сначала создайте таблицу, к которой будете привязывать, а уже потом привязываемую. Иначе Liquibase не поймет, что с чем нужно связывать и выдаст ошибку. В нашем случае мы сначала создаем genre, а уже потом book с его внешним ключом.

Шаг седьмой. Теперь запускаем наш проект. Liquibase использует наши скрипты и создаст все таблицы. В итоге должно получиться так:

Миграции баз данных с помощью библиотеки Liquibase 2

Шаг восьмой. Теперь добавим поле author в уже созданную таблицу book. Для этого нам нужно в changeset-book-table.xml добавить новый чейнжсет и использовать тег addcolumn.

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

			‹changeSet id="add_colum_author" author="mediaSoft" runOnChange="true"> 
  ‹addColumn tableName="book"> 
    ‹column name="author" type="VARCHAR(255)"> 
      ‹constraints nullable="false"> 
      ‹/constraints>
    ‹/column> 
     ‹/addColumn> 
‹/changeset>
		

После запуска проекта мы увидим, что в нашу таблицу добавилось новое поле author.

Миграции баз данных с помощью библиотеки Liquibase 3

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

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

Мы будем использовать откат по тегу, так как это самый удобный и простой способ. В данном случае, тег — это своего рода чекпоинт, по которому Liquibase определяет, что этот чейнджсет был последним в рамках наших изменений. К этой контрольной точке можно вернуться в любой момент, если мы что-то сделаем не так.

Чтобы установить тег, выполним команду в терминале:

			mvn liquibase:tag -Dliquibase.tag=tag_1.
		

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

Миграции баз данных с помощью библиотеки Liquibase 4

Проверим работу тега. Для этого создаем новый чейнджсет на создание таблицы comics по пути src/main/resources/db/changelog/create-changeset-comics-table.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">
   ‹changeSet id="create_table_comics" author="mediaSoft">
       ‹!-- Прописываем создание таблицы comics-->
       ‹createTable tableName="comics">
           ‹!--Создаем поля -->
           ‹column autoIncrement="true" name="comics_id" type="bigint">
               ‹constraints primaryKey="true" nullable="false"/>
           ‹/column>
           ‹column name="comics_name" type="varchar(64)">
               ‹constraints nullable="false" unique="true"/>
           ‹/column>
       ‹/createTable>
   ‹/changeSet>
‹/databaseChangeLog>
		

Добавим наш новый файл в changelog.xml, запустим его и убедимся, что таблица добавилась в базу данных.

Миграции баз данных с помощью библиотеки Liquibase 5
Миграции баз данных с помощью библиотеки Liquibase 6

Теперь, если мы захотим удалить таблицу comics, нам необходимо выполнить эту команду в терминале, указав в конце команды наш тег:

			mvn liquibase:tag -Dliquibase.tag=tag_1.
		

После выполнения команды мы увидим, что таблица вернулась к первоначальному состоянию.

Миграции баз данных с помощью библиотеки Liquibase 7

Ещё несколько советов по работе с Liquibase

Используйте XML, а не YAML или JSON

XML дает возможность автодополнения в IDE и автоматически проверяет формальную верность документа по схеме данных. Так что при написании лучше использовать XML, а к остальным форматам обращаться, если вы хотите быстро проверить миграцию.

Не используйте автоматическую генерацию Liquibase

Liquibase умеет сам генерировать чейнджсеты, на основе существующей базы данных, и показать изменения, произошедшие в БД по сравнению с имеющимися чейнджсетам. К этим удобствам надо относиться с осторожностью. Да, они позволяют избавить от рутины, но даже в этом случае все чейнджсеты надо просматривать и проверять. Вы ведь не хотите исправлять логическую ошибку, сгенерированную утилитой и уехавшую в прод, правда?

Лучше много маленьких чейнджсетов, чем один большой

Один чейнджсет — одна атомарная операция. Если происходит много изменений, лучше, чтобы операции были поменьше. Для каждой маленькой операции мы можем написать свою отдельную проверку. Такое изменение будет проще откатить и не затронуть остальные. А также оно будет проще для понимания другому разработчику в команде.

Работайте над операциями изолированно, не добавляйте без необходимости несколько изменений в один чейнджсет, думайте над содержимым тега rollback. Лучше много простых и даже однотипных чейнджсетов, чем один сложный и невнятный.

Скрипты

Хорошей практикой является хранение скриптов для каждой таблицы отдельно с последующим включением в корневой файл, который будет их запускать. Как и в SQL здесь важен порядок написания запросов, так как они запускаются друг за другом.

По ссылке оставили официальный гайд с лучшими практиками использования Liquibase.

Следите за новыми постами
Следите за новыми постами по любимым темам
30К открытий34К показов