Работа с Liquibase

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

Как работает Liquibase

Работает Liquibase так: программист записывает все SQL-изменения базы данных в специальном Liquibase-формате (xml, yaml, json, sql — на выбор). А библиотека Liquibase (у нас это  maven-зависимость) запускает эти изменения. При этом Liquibase отслеживает саму себя — какие скрипты она уже запускала, а какие еще нет. Для этого она создает в нашей базе две служебные таблицы, в которых хранятся идентификаторы скриптов с датой запуска и т.п.  И при запуске Spring Boot приложения Liquibase  сама решает, какие скрипты выполнять, а какие нет.

databasechangelog хранит когда какие скрипты запускались, (а databasechangeloglock ставит блокировки, если несколько экземпляров приложений поднимаются на одной базе одновременно)
databasechangelog хранит когда какие скрипты запускались, (а databasechangeloglock ставит блокировки, если несколько экземпляров приложения запускаются на одной базе одновременно)

В итоге получается, что при запуске приложения на пустой базе, она заполнится (таблицами и данными, которые есть в скриптах) и накатятся все изменения (которые есть в скриптах). А при запуске приложения на заполненной базе последней версии, скрипты не накатятся. При запуске же приложения на частично устаревшей базе, накатятся только те скрипты, которые еще не выполнялись. Отлично, теперь не надо помнить, на какой базе уже выполнены скрипты, а на какой нет. Устраняется путаница.

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

Перенос существующей базы данных в Liquibase-формат

Это можно сделать с помощью maven-плагина, но мы рассмотрим, как сгенерировать файл со скриптами (у нас он будет называться master.yaml) из командной строки. Для этого нужно установить на компьютер Liquibase.

Затем в любой папке компьютера создаем файл liquibase.properties с:

  • параметрами доступа к базе,
  • путем до драйвера,
  • именем выходного файла (например, master.yaml)
  • названием папки с данными базы.

Например, для PostgreSQL-базы можно положить драйвер postgresql-42.2.14.jar прямо в ту же папку, где находится файл liquibase.properties. А в файле liquibase.properties написать:

username=postgres
password=mypassword
url=jdbc:postgresql://localhost/mydbname
driver=org.postgresql.Driver
classpath=postgresql-42.2.14.jar
changeLogFile=master.yaml

dataOutputDirectory=data

Затем из командной строки выполним:

liquibase generateChangeLog

В итоге получим в этой же папке файл master.yaml с описанием структуры базы, а в папке data этой же папки — данные в формате csv. По одному файлу на таблицу базы.

Чтобы получить скрипты в другом формате, например XML, нужно просто прописать название выходного файла с другим расширением — master.xml.

Пример сгенерированного скрипта

Пусть в нашем приложении есть единственная сущность Animal:

@Entity
public class Animal {
    @Id
    @GeneratedValue(strategy= GenerationType.SEQUENCE)
    private long id;
    private String name;
}

которой соответствует таблица animal в базе. В таблице две строки.

После запуска команды:

liquibase generateChangeLog

содержимое master.yaml будет таким:

databaseChangeLog:
- changeSet:
    id: 1596784098744-1
    author: myluc (generated)
    changes:
    - createTable:
        columns:
        - column:
            constraints:
              nullable: false
              primaryKey: true
              primaryKeyName: animal_pkey
            name: id
            type: BIGINT
        - column:
            name: name
            type: VARCHAR(255)
        tableName: animal
- changeSet:
    id: 1596784098744-2
    author: myluc (generated)
    changes:
    - loadData:
        columns:
        - column:
            header: id
            name: id
            type: NUMERIC
        - column:
            header: name
            name: name
            type: STRING
        commentLineStartsWith: '#'
        encoding: UTF-8
        file: data/animal.csv
        quotchar: '"'
        separator: ','
        tableName: animal
- changeSet:
    id: 1596784098744-3
    author: myluc (generated)
    changes:
    - createSequence:
        cacheSize: 1
        cycle: false
        dataType: bigint
        incrementBy: 1
        maxValue: 9223372036854775807
        minValue: 1
        sequenceName: hibernate_sequence
        startValue: 3

Тут три changeSet (каждому соответствует свой идентификатор):

  • создание таблицы animal
  • заполнение ее данными из файла data/animal.csv
  • создание sequence (для генерации идентификаторов id, так как в сущности @Animal мы используем strategy= GenerationType.SEQUENCE)

Таким образом всё, что есть в базе, перенеслось в набор changeSet-ов.

Теперь внесем в Spring Boot приложение зависимость Liquibase, настроим и ее на запуск полученных changeSet-ов. Так при запуске Spring Boot приложения на пустой базе она будет заполняться таблицами и данными с нуля.

Заполнение базы из Liquibase-файлов с приложением  Spring Boot

Мы слегка переделаем master.yaml, отделив создание таблицы и последовательности от создания данных. Это нужно для того, чтобы в будущем было проще дополнять changeSet-ы со своими изменениями.

Maven-зависимость

Во-первых, надо добавить в Spring Boot приложение Maven-зависимость:

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

Настройки в application.yml

Во-вторых, чтобы библиотека Liquibase выполняла невыполненные скрипты при запуске приложения, в application.yml необходимо ее включить (enabled=true):

spring:
  liquibase:
   enabled: true
   change-log: classpath:db/changelog/db.changelog-master.yaml
   contexts: dev

db/changelog/db.changelog-master.yaml — это имя и путь до нашего master.yaml. Имя/путь длинные, но зато эту строку можно на самом деле не указывать, так как именно это имя и этот путь используется в Liquibase  по умолчанию. Так что переименуем файл master.yaml в файл db.changelog-master.yaml и положим по указанному пути в папку resources.

Структура папок

Таким образом, структура папок такая (наш сгенерированный master.yaml переименован в db.changelog-master.yaml). И сгенерированная папка data с csv-файлом вложена в data:

Структура папок
Структура папок

Мастер-файл db.changelog-master.yaml

Вместо трех changeSet-ов в главный файл db.changelog-master.yaml поместим пути до папок, которые будут пополняться скриптами с со структурой и данными базы (при этом имена файлов не указываем, загружаются все, что есть в папке).

databaseChangeLog:
- includeAll:
    path: db/changelog/1.0/
- includeAll:
    path: db/changelog/data/

По пути db/changelog/1.0/ сейчас лежит один файл 2020-20-02-create-tables.yaml.

2020-20-02-create-tables.yaml — создание структуры базы

В него мы скопируем два сгенерированных changeSet-а из трех: а именно, создание таблицы и создание последовательности. Таким образом, содержимое файла такое:

databaseChangeLog:
  - changeSet:
      id: 1596784098744-1
      author: myluc (generated)
      changes:
        - createTable:
            columns:
              - column:
                  constraints:
                    nullable: false
                    primaryKey: true
                    primaryKeyName: animal_pkey
                  name: id
                  type: BIGINT
              - column:
                  name: name
                  type: VARCHAR(255)
            tableName: animal
  - changeSet:
      id: 1596784098744-3
      author: myluc (generated)
      changes:
        - createSequence:
            cacheSize: 1
            cycle: false
            dataType: bigint
            incrementBy: 1
            maxValue: 9223372036854775807
            minValue: 1
            sequenceName: hibernate_sequence
            startValue: 3

initial-data.yaml — загрузка данных

Файл initial-data.yaml отвечает за загрузку данных. В него скопирован третий changeSet:

databaseChangeLog:
- changeSet:
      id: 1596784098744-2
      author: myluc (generated)
      changes:
        - loadData:
            columns:
              - column:
                  header: id
                  name: id
                  type: NUMERIC
              - column:
                  header: name
                  name: name
                  type: STRING
            commentLineStartsWith: '#'
            encoding: UTF-8
            file: data/animal.csv
            quotchar: '"'
            separator: ','
            relativeToChangelogFile: true
            tableName: animal
Обратите внимание, что копию мы немного поменяли. Тут добавлено свойство relativeToChangelogFile: true. Оно означает, что путь data/animal.csv до файла с данными берется относительно самого файла  initial-data.yaml.

Запуск Spring Boot приложения

Теперь приложение можно запускать на новой пустой базе. (Предварительно нужно только создать саму базу с нужным именем и прописать к ней spring.datasource настройки в application.yml).

При первом запуске Spring Boot приложения Liquibase заполнит базу, а также создаст две служебные таблицы Liquibase. В них сохранится сам факт запуска скриптов с данными идентификаторами. Поэтому при втором запуске Spring Boot приложения на этой базе Liquibase не будет повторно выполнять скрипты.

Как отделить скрипты для тестовой базы

Обычно нужно выполнить в основной базе одни скрипты (changeSet-ы), а в тестовой базе — другие. Для этого в changeSet существует поле context. Например, чтобы данный changeSet, загружающий тестовые данные, выполнялся только в тестовой базе, в его поле context указываем значение test:
- changeSet:
     id: ...
     author: ...
     context: test
     ....

Этот changeSet выполнится только в тестовой базе.

Откуда взялось значение test? Оно прописывается в файле application.yaml, в данном случае в том, что находится в папке тестов test:

liquibase:
  enabled: true
  change-log: classpath:db/changelog/db.changelog-master.yaml
  contexts: test

Если же changeSet надо выполнить в обоих контекстах (в тестовой и основной базе — например, это может быть создание таблицы), то помечаем его так:

- changeSet: 
     id: ... 
     author: ... 
     context: dev or test
     ....

При этом контекст dev прописан в основном  application.yaml папки main:

liquibase:
  enabled: true
  change-log: classpath:db/changelog/db.changelog-master.yaml
  contexts: dev

Итоги

Теперь наше Spring Boot приложение с maven-зависимостью Liquibase, включенной настройкой spring.liquibase.enabled=true и размещенными в ресурсах changeSet-ами можно запускать на пустой базе. При первом запуске база будет заполнена, при последующих запусках останется такой же.

Исходный код примера есть на GitHub: там Spring Boot приложение с готовыми Liquibase-скриптами.

После запуска можно проверить содержимое служебной таблицы databasechangelog. В ней столбец id — это идентификаторы changeSet-ов из yaml-файлов. При первом запуске приложения таблица будет заполнена (как показано на первой картинке статьи). По этой таблице Liquibase будет знать, что changeSet-ы с такими идентификаторами выполнены, и не будет выполнять их во второй раз при новых запусках приложения на этой же базе.

По мере развития проекта и изменения базы новые yaml-файлы можно добавлять в папки db/changelog/1.0/ и db/changelog/data/  — Liquibase подхватит и выполнит эти файлы.

 

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *