Миграции баз данных — что это такое и для чего нужно
Миграция баз данных — это что-то вроде системы контроля версий для вашей схемы базы данных. Она позволяет разработчикам изменять структуру БД, сообщать другим участникам команды об этих изменениям и самим быть в курсе апдейтов, а также отслеживать историю изменений.
Зачем нужны миграции БД и как они упрощают жизнь разработчикам
- По мере разработки приложения схема базы данных меняется. Добавляются новые таблицы и столбцы. Миграции позволяют упростить отслеживание этих изменений.
- У современных проектов зачастую есть несколько стендов — стенд разработки, тестирования, прод и другие. Возникает проблема синхронизации базы данных — нужно передавать изменения на стенды последовательно и без конфликтов. Миграции помогают решить эту проблему.
- Современные проекты работают по методологии Agile, а в ней сложно определить структуру БД на старте. Она развивается вместе с проектом от спринта к спринту. Поэтому автоматизированный рефакторинг БД должен быть таким же обязательным инструментом, как и рефакторинг любых других компонентов.
- Поскольку миграции являются частью исходного кода проекта, изменение структуры БД могут быть одобрены или отклонены во время код ревью до того, как они попадут в релизную ветку.
Чаще всего для настройки миграций БД в 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
Как настроить миграцию уже готовой базы данных
- Подключаем библиотеку Liquibase, то есть добавить зависимость в вашем проекте.
- Пишем нулевой вариант базы данных, то есть описываем скриптами ту БД, которая уже развернута.
- Все последующие изменения в структуре БД вносим только через скрипты.
Как создать миграцию для новой базы данных
Первый шаг. Для работы с библиотекой нам нужно добавить зависимость в файл 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>
Шаг третий. Теперь напишем скрипты для создания таблиц. За основу возьмем схему базы данных ниже.
Описываем чейнджсет для создания таблицы 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 использует наши скрипты и создаст все таблицы. В итоге должно получиться так:
Шаг восьмой. Теперь добавим поле 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, можем переходить к откату изменений.
Многие операции Liquibase может откатить самостоятельно, например, создание таблицы и добавление колонки. Для некоторых чейнджсетов необходимо написать скрипты отката. Мы будем использовать автоматический откат изменений. Остальные способы описаны на официальном сайте Liquibase. Важно понимать, что откат изменений приводит к потере данных.
Мы будем использовать откат по тегу, так как это самый удобный и простой способ. В данном случае, тег — это своего рода чекпоинт, по которому Liquibase определяет, что этот чейнджсет был последним в рамках наших изменений. К этой контрольной точке можно вернуться в любой момент, если мы что-то сделаем не так.
Чтобы установить тег, выполним команду в терминале:
mvn liquibase:tag -Dliquibase.tag=tag_1.
Тег всегда устанавливается на последнем чейнджсете. Теперь если мы посмотрим в таблицу databaseChangelog, увидим, что к последнему чейнджсету привязался tag_1, к которому мы сможем откатиться при необходимости.
Проверим работу тега. Для этого создаем новый чейнджсет на создание таблицы 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, запустим его и убедимся, что таблица добавилась в базу данных.
Теперь, если мы захотим удалить таблицу comics, нам необходимо выполнить эту команду в терминале, указав в конце команды наш тег:
mvn liquibase:tag -Dliquibase.tag=tag_1.
После выполнения команды мы увидим, что таблица вернулась к первоначальному состоянию.
Ещё несколько советов по работе с Liquibase
Используйте XML, а не YAML или JSON
XML дает возможность автодополнения в IDE и автоматически проверяет формальную верность документа по схеме данных. Так что при написании лучше использовать XML, а к остальным форматам обращаться, если вы хотите быстро проверить миграцию.
Не используйте автоматическую генерацию Liquibase
Liquibase умеет сам генерировать чейнджсеты, на основе существующей базы данных, и показать изменения, произошедшие в БД по сравнению с имеющимися чейнджсетам. К этим удобствам надо относиться с осторожностью. Да, они позволяют избавить от рутины, но даже в этом случае все чейнджсеты надо просматривать и проверять. Вы ведь не хотите исправлять логическую ошибку, сгенерированную утилитой и уехавшую в прод, правда?
Лучше много маленьких чейнджсетов, чем один большой
Один чейнджсет — одна атомарная операция. Если происходит много изменений, лучше, чтобы операции были поменьше. Для каждой маленькой операции мы можем написать свою отдельную проверку. Такое изменение будет проще откатить и не затронуть остальные. А также оно будет проще для понимания другому разработчику в команде.
Работайте над операциями изолированно, не добавляйте без необходимости несколько изменений в один чейнджсет, думайте над содержимым тега rollback. Лучше много простых и даже однотипных чейнджсетов, чем один сложный и невнятный.
Скрипты
Хорошей практикой является хранение скриптов для каждой таблицы отдельно с последующим включением в корневой файл, который будет их запускать. Как и в SQL здесь важен порядок написания запросов, так как они запускаются друг за другом.
По ссылке оставили официальный гайд с лучшими практиками использования Liquibase.