[Java, Администрирование баз данных, DevOps] Версионирование структуры БД при помощи Liquibase
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Большая часть приложений, которые мне встречались, хранят данные в SQL базе данных. Если у вас корпоративное приложение, то скорее всего имеется несколько стендов: стенд разработки, пре-прод и прод. А над приложением трудится команда разработчиков.Такие приложения сталкиваются с проблемой синхронизации схемы БД между контурами и самими разработчиками. Надо как-то передать изменения, которые вы внесли всем остальным и при этом не получить конфликты.Эти проблемы решает система управления миграциями Liquibase. Это своего рода система контроля версий вашей базы данных.
Liquibase - независимая от базы данных библиотека для отслеживания, управления и применения изменений схемы базы данных.
Изменения для БД записываются в формате понятном Liquibase, а уже он в свою очередь выполняет запросы к базе данных. Таким образом реализуется независимость от конкретной БД. Liquibase поддерживает 10 типов баз данных, включая DB2, Apache Derby, MySQL, PostgreSQL, Oracle, Microsoft® SQL Server, Sybase и HSQL. Список всех поддерживаемых БД можно посмотреть на сайте.
Существует другие системы управления миграциями: Doctrine 2 migrations, Rails AR migrations, DBDeploy и т.д. Но некоторые из них платформо-зависимые, некоторые не обладают таким широким функционалом.Также серьезный недостаток многих систем — невозможность применения некоторых изменений без потери данных, например, переименование столбца произойдет как две операции: drop + add, что приведет к потере данных.
Как работает LiquibaseLiquibase — кросс платформенное Java приложение, это значит, что вы можете скачать JAR файл и использовать его на Windows, Mac или Linux.Для примера мы будем рассматривать работу со spring-boot приложением и PostgresSQL базой данных. Но вы должны знать, что liquibase можно использовать и отдельно в виде .jar файла. Вот так:
java -jar liquibase.jar --driver=com.mysql.jdbc.Driver--classpath=lib/mysql-connector-java-5.1.21-bin.jar --changeLogFile=/path/to/changelog.yaml --url="jdbc:mysql://localhost/application" --username=dbuser --password=secret update
ChangelogИзменения структуры базы данных записываются в файлы, которые называются changelog. Поддерживаемые форматы: XML, YAML, JSON или SQL.Файлы изменений могут быть произвольно включены друг в друга для лучшего управления. Подробнее об этом ниже.
Я являюсь ярым противником XML конфигураций, но в данном случае это самый удобный формат для записи миграций.
ChangeSetChangeSet – это аналог коммита в системах контроля версий, таких как Git. ChangeSet может содержать одно или несколько изменений базы данных. Хорошей практикой считается одна команда для одного ChangeSet.Каждый changeSet имеет составной идентификатор id и author, который должен быть уникальным.При первом запуске Liquibase создает две технические таблицы:
- databasechangelog – Содержит список изменений схемы БД. Туда записываются уже выполненные changeSet.
- databasechangelock – Используется для блокировки на время работы, чтобы гарантировать одновременную работу только одного экземпляра Liquibase.
БлокировкаЕсли несколько экземпляров Liquibase будут выполняться одновременно с одной и той же базой данных, вы получите конфликты. Это может произойти, если несколько разработчиков используют один и тот же экземпляр базы данных или если в кластере несколько серверов, которые автоматически запускают Liquibase при запуске.Для защиты от таких ситуаций Liquibase создает таблицу databasechangelock, в которой есть boolean поле locked. При запуске Liquibase проверяет его состояние, и если оно true, то ожидает смены на false.Экстренно остановив выполнение программы в самом начале, может сложиться ситуация при котором Liquibase успеет поставить флаг, но не поменяет его на false. В логах это будет выглядеть так:
locked-database.jpgЧтобы исправить эту проблему, в таблице databasechangelock измените поле locked на false.
databasechangelock.jpgКонтрольная суммаДалее Liquibase читает главный changelog, проверяя какие изменения уже были приняты, а какие надо выполнить.После выполнения changeSet в таблицу databasechangelog со всем прочим записывается MD5 хэш changeSet. Хэш высчитывается на основе нормализованного содержимого XML.При следующем запуске Liquibase будет сверять вновь рассчитанные хэш суммы, со значениями в его таблице. Если вы изменили уже выполненный changeSet, то хэш сумма будет отличаться, и приложение упадет с ошибкой при старте.
error-md5.jpg
После выполнения changeset нельзя изменить
Начало работыИ так у нас уже есть spring-boot приложение, в которое мы хотим добавить Liquibase.
Репозиторий с примерами из статьи на GitHub:
https://github.com/Example-uPagge/liqubase
Настройка для spring-bootЧтобы добавить поддержку Liquibase, нужно указать следующие зависимости в maven:
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
Так же в файл application.yml укажем соединение с базой данных:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/liquibase_example
username: postgres
driver-class-name: org.postgresql.Driver
password: password
Если вы используете Hibernate, то не забудьте отключить создание схемы БД.Теперь нам необходимо создать главный changelog. По умолчанию в spring-boot Liquibase ищет его в папке resources/db/changelog/db.changelog-master.yml. Как я уже говорил мы будем использовать XML формат.Создаем файл resources/db/changelog/db.changelog-master.xml. И изменяем путь в application.yml:
spring:
# .. .. .. .. ..
liquibase:
change-log: classpath:db/changelog/db.changelog-master.xml
Вставляем начальное содержимое в файл:
<databaseChangeLog
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
// сюда пишутся changeSets
</databaseChangeLog>
Чтобы быстро получить результат, мы создадим changeSet прямо в этом файле, а потом я расскажу почему так делать не стоит ?Создание таблицыСоздадим таблицу Person.
<changeSet id="create-table-person" author="uPagge">
<createTable tableName="person">
<column name="id" type="int" autoIncrement="true">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="name" type="varchar(64)"/>
<column name="telegram_id" type="int">
<constraints unique="true"/>
</column>
</createTable>
</changeSet>
Тег createTable содержит параметр tableName, который указывает имя новой таблицы. Внутри этого тега мы перечислили колонки, которые нам нужны.Для колонок обязательно необходимо указать тип. Тип указывается в формате Liquibase, после чего он приводится для конкретной реализации БД.Отдельного внимания заслуживает колонка id. Для нее мы задали автоинкремент, а так же в constraints указали ограничения колонки:
- primaryKey="true" – колонка является первичным ключом таблицы.
- nullable="false" – значения не могут быть NULL.При использовании primaryKey параметр nullable не обязателен. Но если вы используете H2 для тестов, то у вас могут возникнуть проблемы из-за его отсутствия.
После запуска spring-boot приложения у нас будет создано 3 таблицы, одна из которых и будет person.Добавление колонки в таблицуА теперь попробуем добавить новую колонку в таблицу в этом changeSet. Изменим его:
<changeSet id="create-table-person" author="uPagge">
<createTable tableName="person">
<column name="id" type="int" autoIncrement="true">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="name" type="varchar(64)"/>
<column name="telegram_id" type="int">
<constraints unique="true"/>
</column>
<column name="address" type="varchar(300)"/>
</createTable>
</changeSet>
Снова запустив приложение мы получим ошибку.Если changeSet уже выполнился, и запись об этом есть в databasechangelog, то вы не можете просто изменить changeSet. Вы же не можете в git изменить уже опубликованный коммит.В этом случае у вас три пути:
- Создать новый changeSet с изменениями. [Рекомендуемый]
- Выполнить откат средствами Liquibase.
- Удалить запись о выполнении changeSet из databasechangelog. Не рекомендую этот вариант, если changeSet уже был выполнен на каком-то контуре. Этот вариант удобен для локальной разработке.
Вернем changeSet в его предыдущее состояние и создадим новый:
<changeSet id="create-table-person" author="uPagge">
<createTable tableName="person">
<column name="id" type="int" autoIncrement="true">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="name" type="varchar(64)"/>
<column name="telegram_id" type="int">
<constraints unique="true"/>
</column>
</createTable>
</changeSet>
<changeSet id="add-new-column-address" author="uPagge">
<addColumn tableName="person">
<column name="address" type="varchar(300)"/>
</addColumn>
</changeSet>
Запускаем приложение. На этот раз успешно, новая колонка добавилась.
add-new-column-success.jpgСвязь с другой таблицейСвязь между таблицами довольно частое явление. Добавим новую таблицу Book и свяжем ее с таблицей Person. Создадим новый changeSet:
<changeSet id="create-table-book" author="uPagge">
<createTable tableName="book">
<column name="id" type="int" autoIncrement="true">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="name" type="varchar(64)"/>
<column name="author_id" type="int">
<constraints foreignKeyName="book_author_id_person_id" references="person(id)"/>
</column>
</createTable>
</changeSet>
Теперь атрибут author_id связан с атрибутом id в таблице Person.При этом обязательно нужно указать уникальный foreignKeyName. Я пользуюсь следующим правилом: имя_таблицы + имя_поля + имя_главной_таблицы + имя_поля_главной_таблицы.Также мы можем указать тип каскадной операции:Теперь атрибут author_id связан с атрибутом id в таблице Person.При этом обязательно нужно указать уникальный foreignKeyName. Я пользуюсь следующим правилом: имя_таблицы + имя_поля + имя_главной_таблицы + имя_поля_главной_таблицы.Также мы можем указать тип каскадной операции:
<constraints foreignKeyName="book_author_id_person_id" references="person(id)" deleteCascade="true"/>
Теперь, если автор книги будет удален, то книга тоже будет удалена.Если вам необходима операция каскадного обновления, то вам нужен второй способ связи с таблицей:
<changeSet id="create-table-book" author="uPagge">
<createTable tableName="book">
<column name="id" type="int" autoIncrement="true">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="name" type="varchar(64)"/>
<column name="author_id" type="int"/>
</createTable>
<addForeignKeyConstraint baseTableName="book" baseColumnNames="author_id"
constraintName="book_author_id_person_id"
referencedTableName="person"
referencedColumnNames="id"
onUpdate="CASCADE"/>
</changeSet>
Создание представленияНесмотря на то, что к этому моменту вы уже полюбили создание изменений с помощью XML, для создания представления придется использовать SQL:
<changeSet id="create-view-book-author" author="uPagge">
<createView viewName="author_and_book">
SELECT p.id as person_id,
p.name as person_name,
b.id as book_id,
b.name as book_name
FROM person p
LEFT JOIN book b on p.id = b.author_id
</createView>
</changeSet>
? Советы от бывалыхПознакомился я с Liquibase на своей стажировке в 2017. С тех пор я использую Liquibase на своих домашних проектах, и продвигаю его использование на рабочих.Мне уже проще написать changeSet, чем SQL. Поэтому далее будет небольшой список рекомендаций, которые облегчат вам жизнь.Организация ChangeSetДобавляя все changeSet в один changelog у вас могло закрасться сомнение: а все ли мы правильно делаем. Схема БД может довольно динамично меняться, особенно в начале создания приложения, поэтому мы ожидаем множество changeSet.Можно создавать множество ChangeLog и включать их друг в друга. Далее я расскажу о своем подходе к организации changelog.Я придерживаюсь следующего подхода:
- Для каждой текущей версии приложения создаем папку в db/changelog. Так как у нас еще нет версии приложения, то будем использовать папку v.1.0.0.
- В этой папке у нас будет локальный главный чейджлог-файл. Я называю их cumulative.xml.
- Когда вам необходимо внести набор изменений для схемы БД, то вы создаете отдельный changelog и включаете его в cumulative.xml.
- В db.changelog-master.xml мы подключаем все cumulative.xml.
Во время выпуска релиза у вас могут оказаться запросы на слияния, которые затрагивают добавления новых changeSet. В этих ПР необходимо создать новую папку для новых changeLogs с номером нового релиза и перенести туда changeLogs для этих ПРов.
Правила именованияПравило именования файлов позволяет без просмотра кумулятивных чейнджлогов файлов понять, что за чем следовало, и не допустит случайного повторения id у changeSet.Вы можете придумать свои правила, но вот что предлагаю я:
- Каждый changelog, кроме cumulative.xml, начинается с текущей даты, а далее короткое описание всех изменений. Например: 2020-03-08-create-tables.xml
- Так же поступайте с id у changeSet. Например id="2020-03-08-create-table-person".
Не изменяйте данныеабота с данными в БД не входит в число ключевых фич Liquibase и ограничивается лишь простейшими операциями вставки и удаления или изменения. Исходя из своего опыта крайне не рекомендую изменять данные с помощью Liquibase.
- Кто-нибудь обязательно ошибется и ошибка уедет на тестовую среду, а откатывать придется вручную.
- Идентификаторы к записям чаще всего генерируются автоматически, что может привести к дополнительным конфликтам.
Используйте XMLИногда хочется «облегчить» жизнь и отказаться от XML, начав использовать более краткий DSL: groovy, yaml, json.Все это очень хорошо до тех пор, пока вам не захочется иметь:
- Авто дополнение в IDE
- Автоматическую проверку формальной верности документа по схеме данных
Используйте remarkРазработчики стараются давать понятные имена для переменных, но дополнительное описание не будет лишним. Параметр remark позволяет добавлять описания к таблицам и полям.
<changeSet id="2021-02-22-create-person" author="uPagge">
<createTable tableName="person" remarks="Пользователи системы">
<column name="id" type="int" autoIncrement="true" remarks="Идентификатор">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="name" type="varchar(64)" remarks="Имя пользователя">
<constraints nullable="false"/>
</column>
<column name="telegram" type="int" remarks="Идентификатор в телеграмм">
<constraints unique="true"/>
</column>
</createTable>
</changeSet>
Итого
Эта же статья в момем блоге: Версионирование структуры БД при помощи Liquibase.
В этой статье мы обсудили логику работы Liquibase, а так же простое внедрение его в ваше приложение. Этого уже достаточно, чтобы начать пользоваться Liquibase.В следующей статье я расскажу, как откатывать изменения.
===========
Источник:
habr.com
===========
Похожие новости:
- [Java, Разработка под Android, Kotlin] Уже сегодня Android MeetUp: VK, Leroy Merlin, FindMyKids, Кухня на районе
- [Программирование, Анализ и проектирование систем, Проектирование и рефакторинг, Микросервисы] Разложение монолита: Декомпозиция БД (часть 1)
- [Программирование, Java, Параллельное программирование, Конференции] Обзор программы JPoint 2021: воркшопы, Spring, «игра вдолгую»
- [Высокая производительность, PostgreSQL, SQL, Администрирование баз данных] DBA: Когда почти закончился serial
- [Программирование, Java] Удаленный доступ к IDE при помощи Projector
- [DevOps] Основы работы с Helm чартами и темплейтами — Часть 2 (перевод)
- [Java, API] Сбор метрик Spring Boot приложения c помощью Prometheus и Grafana
- [Разработка веб-сайтов, JavaScript, ООП, ReactJS, TypeScript] Еще один подход к построению архитектуры на фронте
- [Ненормальное программирование, Разработка веб-сайтов, Программирование, Haskell] Зачем мы транспилируем Haskell в JavaScript
- [IT-инфраструктура, DevOps, Tarantool] Как эксплуатировать приложения на Tarantool Cartridge
Теги для поиска: #_java, #_administrirovanie_baz_dannyh (Администрирование баз данных), #_devops, #_java, #_bazy_dannyh (базы данных), #_liquibase, #_spring_boot, #_java, #_administrirovanie_baz_dannyh (
Администрирование баз данных
), #_devops
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:15
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Большая часть приложений, которые мне встречались, хранят данные в SQL базе данных. Если у вас корпоративное приложение, то скорее всего имеется несколько стендов: стенд разработки, пре-прод и прод. А над приложением трудится команда разработчиков.Такие приложения сталкиваются с проблемой синхронизации схемы БД между контурами и самими разработчиками. Надо как-то передать изменения, которые вы внесли всем остальным и при этом не получить конфликты.Эти проблемы решает система управления миграциями Liquibase. Это своего рода система контроля версий вашей базы данных. Liquibase - независимая от базы данных библиотека для отслеживания, управления и применения изменений схемы базы данных.
Существует другие системы управления миграциями: Doctrine 2 migrations, Rails AR migrations, DBDeploy и т.д. Но некоторые из них платформо-зависимые, некоторые не обладают таким широким функционалом.Также серьезный недостаток многих систем — невозможность применения некоторых изменений без потери данных, например, переименование столбца произойдет как две операции: drop + add, что приведет к потере данных.
java -jar liquibase.jar --driver=com.mysql.jdbc.Driver--classpath=lib/mysql-connector-java-5.1.21-bin.jar --changeLogFile=/path/to/changelog.yaml --url="jdbc:mysql://localhost/application" --username=dbuser --password=secret update
Я являюсь ярым противником XML конфигураций, но в данном случае это самый удобный формат для записи миграций.
locked-database.jpgЧтобы исправить эту проблему, в таблице databasechangelock измените поле locked на false. databasechangelock.jpgКонтрольная суммаДалее Liquibase читает главный changelog, проверяя какие изменения уже были приняты, а какие надо выполнить.После выполнения changeSet в таблицу databasechangelog со всем прочим записывается MD5 хэш changeSet. Хэш высчитывается на основе нормализованного содержимого XML.При следующем запуске Liquibase будет сверять вновь рассчитанные хэш суммы, со значениями в его таблице. Если вы изменили уже выполненный changeSet, то хэш сумма будет отличаться, и приложение упадет с ошибкой при старте. error-md5.jpg После выполнения changeset нельзя изменить
Репозиторий с примерами из статьи на GitHub:
https://github.com/Example-uPagge/liqubase <dependency>
<groupId>org.liquibase</groupId> <artifactId>liquibase-core</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> spring:
datasource: url: jdbc:postgresql://localhost:5432/liquibase_example username: postgres driver-class-name: org.postgresql.Driver password: password spring:
# .. .. .. .. .. liquibase: change-log: classpath:db/changelog/db.changelog-master.xml <databaseChangeLog
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd"> // сюда пишутся changeSets </databaseChangeLog> <changeSet id="create-table-person" author="uPagge">
<createTable tableName="person"> <column name="id" type="int" autoIncrement="true"> <constraints nullable="false" primaryKey="true"/> </column> <column name="name" type="varchar(64)"/> <column name="telegram_id" type="int"> <constraints unique="true"/> </column> </createTable> </changeSet>
<changeSet id="create-table-person" author="uPagge">
<createTable tableName="person"> <column name="id" type="int" autoIncrement="true"> <constraints nullable="false" primaryKey="true"/> </column> <column name="name" type="varchar(64)"/> <column name="telegram_id" type="int"> <constraints unique="true"/> </column> <column name="address" type="varchar(300)"/> </createTable> </changeSet>
<changeSet id="create-table-person" author="uPagge">
<createTable tableName="person"> <column name="id" type="int" autoIncrement="true"> <constraints nullable="false" primaryKey="true"/> </column> <column name="name" type="varchar(64)"/> <column name="telegram_id" type="int"> <constraints unique="true"/> </column> </createTable> </changeSet> <changeSet id="add-new-column-address" author="uPagge"> <addColumn tableName="person"> <column name="address" type="varchar(300)"/> </addColumn> </changeSet> add-new-column-success.jpgСвязь с другой таблицейСвязь между таблицами довольно частое явление. Добавим новую таблицу Book и свяжем ее с таблицей Person. Создадим новый changeSet: <changeSet id="create-table-book" author="uPagge">
<createTable tableName="book"> <column name="id" type="int" autoIncrement="true"> <constraints nullable="false" primaryKey="true"/> </column> <column name="name" type="varchar(64)"/> <column name="author_id" type="int"> <constraints foreignKeyName="book_author_id_person_id" references="person(id)"/> </column> </createTable> </changeSet> <constraints foreignKeyName="book_author_id_person_id" references="person(id)" deleteCascade="true"/>
<changeSet id="create-table-book" author="uPagge">
<createTable tableName="book"> <column name="id" type="int" autoIncrement="true"> <constraints nullable="false" primaryKey="true"/> </column> <column name="name" type="varchar(64)"/> <column name="author_id" type="int"/> </createTable> <addForeignKeyConstraint baseTableName="book" baseColumnNames="author_id" constraintName="book_author_id_person_id" referencedTableName="person" referencedColumnNames="id" onUpdate="CASCADE"/> </changeSet> <changeSet id="create-view-book-author" author="uPagge">
<createView viewName="author_and_book"> SELECT p.id as person_id, p.name as person_name, b.id as book_id, b.name as book_name FROM person p LEFT JOIN book b on p.id = b.author_id </createView> </changeSet>
Во время выпуска релиза у вас могут оказаться запросы на слияния, которые затрагивают добавления новых changeSet. В этих ПР необходимо создать новую папку для новых changeLogs с номером нового релиза и перенести туда changeLogs для этих ПРов.
<changeSet id="2021-02-22-create-person" author="uPagge">
<createTable tableName="person" remarks="Пользователи системы"> <column name="id" type="int" autoIncrement="true" remarks="Идентификатор"> <constraints nullable="false" primaryKey="true"/> </column> <column name="name" type="varchar(64)" remarks="Имя пользователя"> <constraints nullable="false"/> </column> <column name="telegram" type="int" remarks="Идентификатор в телеграмм"> <constraints unique="true"/> </column> </createTable> </changeSet> Эта же статья в момем блоге: Версионирование структуры БД при помощи Liquibase.
=========== Источник: habr.com =========== Похожие новости:
Администрирование баз данных ), #_devops |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:15
Часовой пояс: UTC + 5