Кратко о миграциях версий баз данных
Самое простое объяснение - это аналог git, только версионированию подвергнута структура базы данных, в некоторых случаях ее базовое наполнение.
Использование миграций делает возможным:
-
всей команде одного проекта иметь одинаковую структуру БД, обновления которой поставляются вместе с новой версией приложения и автоматически обновляют структуру БД.
-
создавать первоначальную структуру БД
-
заполнить БД минимальным обязательным набором данных.
-
отказаться от ручной работы с базой данных.
Миграции не зря похожи на git, они слой за слоем последовательно “накатываются” в базу данных в известном порядке. Если в базе уже есть часть выполненных миграций, то будут использоваться только новые.
Информация, какие миграции были применены к базе данных, по умолчанию находится в этой же БД, в специальной таблице.
Начальные условия
В этом гайде вы можете использовать готовый проект Spring Boot со сборщиком Gradle (ветка master) или создать с нуля по рекомендациям ниже.
Создание проекта с нуля
Или создать пустой проект Spring Boot с зависимостями и версиями:
plugins {
id 'org.springframework.boot' version '2.5.4'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.liquibase:liquibase-core'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
В качестве БД будем использовать h2 - база данных SQL будет в отдельном файле и через веб интерфейс будем наблюдать за изменениями.
Для старта отключите liquibase, установив параметр в application.yml
:
spring:
liquibase:
enabled: false
spring.liquibase.enabled = false
Если это не сделать, то наше приложение не запустится, так как не созданы конфигурации миграций.
***************************
APPLICATION FAILED TO START
***************************
Description:
Liquibase failed to start because no changelog could be found at 'classpath:/db/changelog/db.changelog-master.yaml'.
Action:
Make sure a Liquibase changelog is present at the configured path.
Настройка и подключение h2
Для включения и доступа к h2 через веб интерфейс добавьте настройки в application.yml
:
spring:
liquibase:
enabled: false
datasource:
url: jdbc:h2:file:./db # в корне проект файл бд db.mv.db
username: u # имя пользователя консоли
password: 1 # пароль консоли
jpa:
database-platform: org.hibernate.dialect.H2Dialect
h2:
console:
enabled: true
Для application.properties
:
spring.liquibase.enabled=false
spring.datasource.url=jdbc:h2:file:./db
spring.datasource.username=u
spring.datasource.password=1
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
После запуска приложения перейдите по ссылке http://localhost:8080/h2-console и попадете в консоль управления базой данных. Введите данные URL, логин и пароль из настройки и после нажатия Connect попадете в упраление базой данных.
Создание Entity классов
Создайте два класса со связями, пусть это будет статьи и их авторы. У автора может быть множество статей, а у статьи только один автор.
Код каждого класса (геттеры и сеттеры не показаны):
@Entity
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
long id;
@Column(nullable = false, length = 100)
String title;
@Column(nullable = false)
String text;
@ManyToOne
Author author;
// getters & setters
}
@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@Column(nullable = false)
String name;
// getters & setters
}
Работа Hibernate
Пропустите этот раздел, если вы знаете как Hibernate обновляет и изменяет структуру БД.
Как можно управлять структурой базы данных через Hibernate? Есть параметр, который указывает что делает Hibernate при запуске приложения:
yml config:
spring:
jpa:
hibernate:
ddl-auto: none;
properties config:
spring.jpa.hibernate.ddl-auto = none
По-умолчанию стоит значение none
, то есть не делать ничего.
Кроме этого есть такие параметры как:
create
- создавать структуру базы данных на основе java классов @Entity. Уничтожает структуру базу данных со всеми данными перед созданием.update
- обновить при возможности структуру базы данные на основе java классов @Entity. При возможности сохраняются данные.validate
- проверить, соответсвует ли существующая структура базы данных написанным классам @Entity. Если не соответсвует, приложение не запустится.none
- не делать ничего. Даже если структуры базы данных нет, то приложения запустится и завершится с ошибкой при первом доступе к бд.
Остальные параметры вы можете посмотреть в статье Vlad Mihalcea
С одной стороны, это открывает возможности всегда поддерживать структуру бд в соответсвии с написанным кодом. И да, это удобно на самом старте проекта, когда и бд небольшая и работаете вы одни над проектом. Данных никаких нет, можно каждый раз обнулять БД.
При этом нам придется мириться с тем:
- как Hibernate будет создавать структуру
- какие ключи прописывать
- как строить связи
- какие значения по умолчанию использовать
Для наглядности, включите параметр create
, запустите программу,
после успешного старта завершите работу программы и зайдите в бд.
Все на месте, как и написали. Есть связи, пусть и Hibernate
назвал их на свой лад, есть все поля, первичные ключи.
Продолжаем работать, переключаем параметр в update
чтобы не
удалить данные при следующем запуске и поддерживать структуру
в актуальном состоянии.
Теперь поменяйте класс Author
, нам понадобилось не
одно имя хранить, а полное имя автора:
@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@Column(nullable = false)
String fullname;
// getters & setters
}
Тоже самое будет, если использовать
@Column(name = "fullname")
запускайте приложение снова и заходите в консоль. Мы ожидаем, что поле name будет переименовано в fullname. Но Hibernate просто создал еще одну колонку, старая осталась.
А если у нас были бы данные в name, то нам бы хотелось чтобы и данные остались, а колонка была бы переименована.
И снова, на старте разработки, меняем на create
и получаем чистую структуру. Никаких проблем!
А если у нас есть база, с данными, и нам надо ее изменить, при этом сохранить данные, а также чтобы все действия были повторены не только локально, но и у каждого участника команды проекта? Тут и приходит на помощь миграция версий баз данных. Даже на таком простом примере, уже виден профит.
Перед следующей главой, вернитесь к исходной версии кода, где у автора было поле
name
.
Пишем первую миграцию для Liquibase
В структуре миграций два основых типа файлов:
-
Файл
changelog
(мастер файл) - содержит список миграций (changeset), которые применяются к базе данных. Миграции выполняются последовательно, в том порядке в котором записаны. Возможные форматы файла: SQL, XML, JSON, YAML. Имя по умолчанию:db.changelog-master
. в статье Vlad Mihalcea -
Файлы
changeset
(файлы миграций) - каждый из файлов содержит набор изменений, которвые применяются к базе данных. На эти файлы ссылаетсяchagelog
. Если при выполнении миграций, файл был уже применен к базе данных - прописанные изменения в нем игнорируются. Возможные форматы файла: SQL, XML, JSON, YAML. Документация
Liquibase при старте работы ищет файл с названиемdb.changelog-master
в
директории src/main/resources/db/changelog
.
Если вы используете IDEA, то убедитесь что вы создали папки db и в ней папку changelog, а не одну папку с названием db.changelog
Создайте файл db.changelog-master.yaml
.
Полный путь до файла будет
src/main/resources/db/changelog/db.changelog-master.yaml
Добавьте в него запись о первом файле миграции:
databaseChangeLog: #параметр в котором находятся миграции
- include: # список активных миграций
file: db/changelog/changeset/create-author-table.yaml # путь к файлу миграции
Файлы с содержимым миграций changeset
могут располагаться как в той же папке что и changelog
,
так и в отдельной директории. Рекомендуется отдельная директория, создайте
для них директорию changeset
в src/main/resources/db/changelog
.
Создайте указанный файл миграции create-author-table.yaml
в директории src/main/resources/db/changelog/changeset
, полный путь до файла будет
src/main/resources/db/changelog/changeset/create-author-table.yaml
databaseChangeLog:
- changeSet:
id: create-author #текстовый идентификатор (Обязателен)
author: Konstantin Shibkov # автор (Обязателен)
changes:
- createTable: # создаем новую таблицу
tableName: author
columns: # объявления колонок
- column:
name: id
type: bigint
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: name
type: varchar(200)
constraints:
nullable: false
скачать файл create-author-table.yaml
Измените параметр liquibase.enabled
в настройках проекта application на true:
liquibase:
enabled: true
Если у вас осталась база данных db.mv.db - удалите ее. При использовании H2 базы данных, пустой файл базы данных будет создан вновь.
Запустите проект, посмотрите в логи, при успехе у вас должна быть такая строка:
2021-08-29 00:26:38.133 INFO 853778 --- [main]
liquibase.changelog
: ChangeSet db/changelog/changeset/create-author-table.yaml
::create-author::Konstantin Shibkov ran successfully in 5ms
а значит миграция накатилась успешно. В противном случае, проверьте названия файлов, синтаксис файлов конфигураций.
Зайдите в консоль h2 http://localhost:8080/h2-console и откройте список таблиц.
В базе будет три таблицы:
author
- таблица для хранения авторовdatabasechangelog
- хранится информация о примененых миграцияхdatabasechangeloglock
- таблица блокировка, которая гарантирует работу одного экземпляра Liquibase при применение миграций.
Посмотрите структуру таблицы author
- она соотвествует прописанному в файле.
Вторая миграция и валидация Hibernate
Предлагаю вам теперь поменять параметр Hibernate ddl-auto на validate:
hibernate:
ddl-auto: validate
и снова запустите проект. Проект не запуститься и посмотрим на логи, показаны избранные строки:
2021-08-29 01:26:13.441 ERROR 915296 --- [main]
o.s.boot.SpringApplication: Application run failed
...
nested exception is
org.hibernate.tool.schema.spi.SchemaManagementException:
Schema-validation: missing table [article]
Причина отказа в запуске явно прописана: в базе данных не найдена таблица article
.
Добавьте новый файл миграции create-article-table.yaml
, с описанием новой таблицы article
:
databaseChangeLog:
- changeSet:
id: create-article
author: Konstantin Shibkov
changes:
- createTable:
tableName: article
columns:
- column:
name: id
type: bigint
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: title
type: varchar(100)
constraints:
nullable: false
- column:
name: text
type: varchar(255)
constraints:
nullable: false
- column:
name: author_id
type: bigint
constraints:
foreignKeyName: author_article_fk
referencedTableName: author
referencedColumnNames: id
и впишите новую строку с этой миграцией в файл changeset
:
- include:
file: db/changelog/changeset/create-article-table.yaml
Запускайте проект, если не запустился и ругается на миграции. Удалите из корня проекта базу данных.
После запуска, переходите в консоль h2 и там вас ожидают две таблицы, со внешним ключом. А если посмотреть в таблицу DATABASECHANGELOG
SELECT * FROM DATABASECHANGELOG
в ней две записи, о каждой миграции которые были применены к базе данных.
Базовая работа с миграциями завершена, если вам надо создать новую таблицу или поменять текущие - пишите миграции, новые миграции версий бд буду исполняться при запуске приложения.
Внесение изменений в таблицу с помощью миграций
А теперь повторите ситуацию, когда надо изменить имя столбца, для этого вам потребуется написать новую миграцию, поправить Entity класс и снова запустить проект:
- новая миграция
databaseChangeLog:
- changeSet:
id: change-column-author
author: Konstantin Shibkov
changes:
- renameColumn:
tableName: author
newColumnName: fullname
oldColumnName: name
- в классе Author укажите новое название поля, можно указать это в анотации
@Column(name = "fullname", nullable = false)
String name;
- обязательно добавьте миграцию в
db.changelog-master.yaml
:
- include:
file: db/changelog/changeset/rename-author-name-to-fullname.yaml
Стартуйте и в консоли h2 увидите измененную таблицу:
Дублирования нет, произошло переименование таблицы, если там были данные, они не пострадали.
Готовый код
После всех предложенных действий код найдете в репозитории, в ветке result готовый проект по рекомендациям из статьи
Типичные шаги разработчика при работе с миграцией
-
пишите новый changeset
-
запускайте миграцию и проверяете, что она работает
-
вносите изменения в код Java приложения
-
проверяете на соответcтвие кода и миграции
-
в одном коммите фиксируете код и миграцию вместе и одновременно
Вольный перевод блока статьи Some best practices to keep in mind when using Liquibase - Typical procedure for the developer
Что в итоге?
-
миграции баз данных позволяют контролировать состояние бд, и распространять изменения среди команды
-
каждый файл миграции исполняется в базе данных один раз.
-
файлы миграций можно писать на SQL, XML, YAML, JSON
-
контроль над структурой базы данных должен остатьтся только у liquibase, ORM только могуть проверять соотвествие своего кода и структуры бд
-
миграции можно откатывать, писать rollback скрипты
Да будут ваши структуры базы данных целостными!