Как стать автором
Обновить

Использование Liquibase для управления структурой БД в Spring Boot приложении. Часть 2

Время на прочтение7 мин
Количество просмотров40K
В предыдущей части мы разобрались с основными возможностями Liquibase и написали базовый пример Spring boot приложения, который использует Liquibase для инициализации БД. Полный код базового приложения можно увидеть тут на GitHub. В этой статье мы поговорим про liquibase-maven-plugin и те дополнительные возможности, которые он нам дает для версионирования структуры БД. Начнем с того, как автоматически создавать скрипты при помощи функции сравнения.

Предположим, что нам понадобилось внести какие-либо изменения в структуру нашей БД. Например, мы хотим, чтобы email не мог быть null. Безусловно, для такого маленького изменения можно было бы подкорректировать код и скрипты вручную, но как быть если изменений будет больше? В этом случае к нам на помощь придет встроенная в Liquibase возможность сравнения БД. Интересной её особенностью является то, что сравнивать можно не только две базы данных, но и базу данных с набором JPA сущностей в нашем приложении. Именно так мы сейчас и поступим!

Создаем скрипт с изменениями при помощи liquibase-diff


В раздел plugins файла pom.xml добавляем вот такую довольно сложную конструкцию. Это liquibase-maven-plugin, к которому подключена зависимость для анализ hibernate-сущностей и работы с файлами в формате YAML. Плагин поможет нам автоматически генерировать liquibase-скрипты через сравнение структур в двух БД или даже через сравнение структуры данных в БД и набора hiberante-сущностей в нашем приложении (именно для этого добавлен liquibase-hibernate5).

<plugin>
    <groupId>org.liquibase</groupId>
    <artifactId>liquibase-maven-plugin</artifactId>
    <version>3.8.1</version>
    <configuration>
        <propertyFile>${project.build.outputDirectory}/liquibase-maven-plugin.properties</propertyFile>
        <systemProperties>
            <user.name>your_liquibase_username</user.name>
        </systemProperties>
        <logging>info</logging>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.liquibase.ext</groupId>
            <artifactId>liquibase-hibernate5</artifactId>
            <version>3.6</version>
         </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
            <version>2.2.0.RELEASE</version>
        </dependency>
    </dependencies>
</plugin>

Стоит обратить внимание на настройку user.name. Она не обязательна, но без нее Liquibase, будет указывать в создаваемых скриптах имя текущего пользователя ОС под которым плагин запущен на выполнение.

Настройки для плагина можно прописать непосредственно в pom.xml или передать как параметры командной строки при вызове maven, но мне больше нравится вариант с отдельным файлом liquibase-maven-plugin.properties. Его содержимое будет выглядеть примерно так.

changeLogFile= @project.basedir@/src/main/resources/db/changelog/db.changelog-master.yaml
url= jdbc:mysql://localhost:3306/geek_db?createDatabaseIfNotExist=true&allowPublicKeyRetrieval=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username= dbusername
password= dbpassword
driver= com.mysql.cj.jdbc.Driver
referenceUrl= hibernate:spring:ru.usharik.liquibase.demo.persist.model?dialect=org.hibernate.dialect.MySQLDialect
diffChangeLogFile= @project.basedir@/src/main/resources/db/changelog/db.changelog-@timestamp@.yaml
ignoreClasspathPrefix= true
Здесь стоит обратить внимание на параметры url и referenceUrl. Скрипт, который создаст liquibase после сравнения будет представлять из себя разницу между базой по ссылке referenceUrlи базой по ссылке url. Если потом этот скрипт запустить на базе по ссылке url, то она станет такой же как и та, которая находится по ссылке referenceUrl. Особое внимание стоит обратить на ссылку referenceUrl. Как видите она ссылается не на БД, а на пакет нашего приложения в котором находятся классы сущностей. Благодаря этому мы сейчас сможем найти скрипт, который добавит в БД изменения, которые были сделаны в коде.

Теперь нам нужно настроить maven-resource-plugin для замены плейсхолдеров в фале настроек, таких как @project.basedir@ и @timestamp@. Для этого добавим в раздел build раздел resources следующего вида

<resources>
    <resource>
        <directory>src/main/resources</directory>
        <filtering>true</filtering>
        <includes>
            <include>*.properties</include>
        </includes>
    </resource>
    <resource>
        <directory>src/main/resources</directory>
        <filtering>false</filtering>
        <includes>
            <include>**/*.*</include>
        </includes>
    </resource>
</resources>

К слову, Spring boot изменяет стандартный формат для плейсхолдеров, заполняемых при помощи maven-resource-plugin с ${smth} на @smth@. Дело в том, что плейсхолдеры вида @smth@ в Spring Boot используются для для подстановки переменных окружения и параметров самого Spring Boot приложения.

Также немного меняем раздел properties в pom.xml, чтобы присвоить значение переменной timestamp в нужном нам формате. Увы, стандартный формат может содержать символы, которые запрещены в именах файлов для некоторых ОС.

<properties>
    <java.version>1.8</java.version>
    <timestamp>${maven.build.timestamp}</timestamp>
    <maven.build.timestamp.format>yyyyMMdd-HHmmssSSS</maven.build.timestamp.format>
</properties>

Теперь давайте изменим поле email в классе User

    @Column(name = "email", nullable = false)
    private String email;

И наконец запустим команду сборки, использующую liquibase-maven-plugin для сравнения.

mvn clean install liquibase:diff -DskipTests=true

В данном случае нам нужно полностью пересобрать проект, потому что плагин (liquibase:diff) будет использовать для анализа не исходники, а скомпилированные файлы классов сущностей из папке target.

Если все сделано правильно, то после успешного выполнения команды в папке resources/db/changelog у вас появится файл с именем вида db.changelog-20190723-100748666.yaml. Благодаря тому, что мы используем текущую дату и время в имени файла, при каждом запуске у нас будет появляться новый файл, что довольно удобно. Если у вас уже создана БД со структурой таблиц, соответствующей прошлому уроку, то содержимое файла должно быть таким.

databaseChangeLog:
- changeSet:
    id: 1563876485764-1
    author: your_liquibase_username (generated)
    changes:
    - addNotNullConstraint:
        columnDataType: varchar(255)
        columnName: email
        tableName: users
Как видите, этот скрипт вносит именно то изменение, которое и было сделано в коде. В качестве упражнения, рекомендовал бы вам запустить данный скрипт против пустой базы данных и посмотреть на результат.

Далее мы можем просто скопировать changeSet из этого файла в db.changelog-master.yaml или можем подключить данный файл в него инструкцией

  - include:
      file: db.changelog-20190723-100748666.yaml
      relativeToChangelogFile: true

Также в этом файле нужно указать logicalFilePath: db/changelog/db.changelog-20190723-100748666.yaml по аналогии с тем, как это сделано в db.changelog-master.yaml.
Это позволит справится с некоторыми проблемами, которые возможны при совместном использовании встроенного в приложение liquibase бина и liquibase-maven-plugin. После этого перезапустите приложение или выполните команду:

mvn liquibase:update

Давайте попробуем внести какое-то более сложное изменение в код. Например добавим таблицу ролей у которой будет связь типа многие ко многим с таблицей пользователей.

@Entity
@Table(name = "roles")
public class Role implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "name", unique = true, nullable = false)
    private String name;

    @ManyToMany(mappedBy = "roles")
    private Set<User> users;

   // далее конструкторы, геттеры, сеттеры
}

А в таблицу Users добавляем
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "users_roles",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id"))
    private Set<Role> roles;

После запуска сравнения получим файл со следующим содержимым
databaseChangeLog:
- changeSet:
    id: 1563877765929-1
    author: your_liquibase_username (generated)
    changes:
    - createTable:
        columns:
        - column:
            autoIncrement: true
            constraints:
              primaryKey: true
              primaryKeyName: rolesPK
            name: id
            type: BIGINT
        - column:
            constraints:
              nullable: false
            name: name
            type: VARCHAR(255)
        tableName: roles
- changeSet:
    id: 1563877765929-2
    author: your_liquibase_username (generated)
    changes:
    - createTable:
        columns:
        - column:
            constraints:
              nullable: false
            name: user_id
            type: BIGINT
        - column:
            constraints:
              nullable: false
            name: role_id
            type: BIGINT
        tableName: users_roles
- changeSet:
    id: 1563877765929-3
    author: your_liquibase_username (generated)
    changes:
    - addPrimaryKey:
        columnNames: user_id, role_id
        tableName: users_roles
- changeSet:
    id: 1563877765929-4
    author: your_liquibase_username (generated)
    changes:
    - addUniqueConstraint:
        columnNames: name
        constraintName: UC_ROLESNAME_COL
        tableName: roles
- changeSet:
    id: 1563877765929-5
    author: your_liquibase_username (generated)
    changes:
    - addForeignKeyConstraint:
        baseColumnNames: user_id
        baseTableName: users_roles
        constraintName: FK2o0jvgh89lemvvo17cbqvdxaa
        deferrable: false
        initiallyDeferred: false
        referencedColumnNames: id
        referencedTableName: users
- changeSet:
    id: 1563877765929-6
    author: your_liquibase_username (generated)
    changes:
    - addForeignKeyConstraint:
        baseColumnNames: role_id
        baseTableName: users_roles
        constraintName: FKj6m8fwv7oqv74fcehir1a9ffy
        deferrable: false
        initiallyDeferred: false
        referencedColumnNames: id
        referencedTableName: roles

Этот файл мы тоже можем легко добавить к рабочим скриптам.

Откат изменений


Теперь давайте посмотрим, как откатить внесенные изменения. По какой-то причине те идентификаторы которые мы указывали в changeSet-ах не могут быть использованы для отката к ним. Есть три варианта, указать точку отката

  • через количество changeSet-ов считая от текущего
  • через дату выполнения изменений
  • через tag (задается при помощи changeSet-а специального вида)

Тег устанавливается следующим образом.

  - changeSet:
      id: some_uniqui_id
      author: liquibase_user_name
      changes:
        - tagDatabase:
            tag: db_tag

Ну и команды для трех перечисленных способов сделать rollback

mvn liquibase:rollback -Dliquibase.rollbackTag=db_tag
mvn liquibase:rollback -Dliquibase.rollbackCount=1
mvn liquibase:rollback "-Dliquibase.rollbackDate=Jun 03, 2017"

Ну и напоследок некоторые дополнительные материалы

  1. Код к данной статье
  2. Про rollback в Liquibase
  3. Про миграцию при помощи Liquibase
  4. Liquibase на GitHub
  5. Очень хорошая статья о различных подходах к версионированию БД

Разумеется, буду очень рад любым замечаниям, дополнениям, уточнениям и т.д.
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 8: ↑8 и ↓0+8
Комментарии12

Публикации

Истории

Работа

Java разработчик
272 вакансии

Ближайшие события

2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань