Статьи

Работа с Liquibase в Spring Boot

Вернуться назад

10 октября 2020

Тэги: gradle Spring Boot SQL PostgreSQL Kotlin

Содержание

  1. Подключаем liquibase к приложению
  2. Создание таблиц
  3. Вставка данных в таблицу
  4. Добавление новой колонки и внешнего ключа
  5. Откат изменений
  6. Выводы

Liquibase позволяет автоматизировать внесение обновлений в структуру БД. Каждое изменение описывается в декларативном стиле и версионируется. Обновления накатываются в заранее определённом порядке на данную БД, если они ещё не накатывались. Автоматизация процесса наката изменений на базу данных особенно важна, если у вас несколько различных экземпляров приложений и для каждого из них требуется поддерживать свою БД.

Подключаем liquibase к приложению

Рассмотрим работу с Liquibase на конкретном примере. С помощью сайта start.spring.io создадим заготовку нашего Spring Boot приложения (выбираем в качестве языка kotlin, а в качестве сборщика - gradle). В dependencies выберем компоненты Spring Web (функциональность rest-контроллеров), Spring Data JDBC (работа с БД), PostgreSQL Driver (драйвер нашей СУБД) и cам Liquibase Migration. В итоге файл build.gradle.kts в секции dependencies должен содержать следующие зависимости:

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
    implementation("org.liquibase:liquibase-core")
    runtimeOnly("org.postgresql:postgresql")

Обратите внимание, что драйвер postgres добавился в gradle как runtimeOnly - это означает, что для компиляции он не требуется, т.к. мы лишь пропишем его в файле настроек.

Для подключения к БД добавим следующие настройки в файл application.yml (не забудьте подставить свои параметры):

spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://localhost:5432/example_db
    username: example_user
    password: "paSSw0rd"
  jpa:
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQL95Dialect
        show_sql: false
        temp:
          use_jdbc_metadata_defaults: false
  liquibase:
    change-log: classpath:liquibase/changelog.yml

Параметр spring.liquibase.change-log в явном виде указывает расположение файла изменений liquibase. В данном случае он должен лежать в папке resources/liquibase/. Можно и не указывать этот параметр в настройках. Тогда изменения надо будет хранить в файле resources/db/changelog/db.changelog-master.yaml.

Предположим, наше приложение является блогом, в котором основная сущность - это статья. У каждой статьи должен быть уникальный идентификатор, url, по которому можно найти её в блоге, заголовок, признак видимости для пользователей и дата создания. Статьи будут храниться в таблице article в БД. Для создания таблиц будем использовать liquibase. Можно было бы сделать и наоборот: сначала создать таблицу в БД вручную, а затем импортировать её структуру в liquibase. Но я рекомендую делать создание именно через liquibase - так у вас будет больше контроля над всеми нюансами.

Изменения, описанные в файле liquibase, будут накатываться при старте приложения. Если изменение уже накатывалось, повторного наката не будет. Историю изменений конкретной БД liquibase хранит в служебной таблице databasechangelog, которая будет создана при первом запуске приложения. Работа с этой таблицей происходит автоматически.

Для внесения изменения в БД требуется добавить новый changeSet в файл liquibase, который в декларативном стиле описывает необходимые изменения.

Создание таблиц

Теперь давайте создадим таблицу article. Добавим в файл changelog.yml первый набор изменений (changeSet) в формате yaml. Помимо yaml liquibase также поддерживает формат xml и json. Но мы будем рассматривать именно yaml как наиболее современный и компактный. В нём важно соблюдать правильное количество пробелов в отступах и не забывать про пробелы перед значениями после двоеточий.

databaseChangeLog:

  - changeSet:
      id: DEV-1
      author: devmarkru
      changes:
        - createTable:
            tableName: article
            remarks: Статьи
            columns:
              - column:
                  name: id
                  type: integer
                  autoIncrement: true
                  constraints:
                    primaryKey: true
                    primaryKeyName: article_pk
                    nullable: false
              - column:
                  name: url
                  type: varchar(50)
                  remarks: Постоянная ссылка на статью
                  constraints:
                    unique: true
                    uniqueConstraintName: article_url_uq
                    nullable: false
              - column:
                  name: title
                  type: varchar(50)
                  constraints:
                    nullable: false
              - column:
                  name: is_visible
                  type: boolean
                  defaultValue: false
                  constraints:
                    nullable: false
              - column:
                  name: created
                  type: timestamp without time zone
                  defaultValue: now()
                  constraints:
                    nullable: false

Файл changelog начинается с корневого элемента databaseChangeLog. Затем мы создаём changeSet, у которого есть текстовый идентификатор (id), автор (author) и список изменений (changes). В качестве идентификатора можно указывать любую произвольную строку, но если вы работаете, например, с Jira, то таким идентификатором может быть номер задачи. В качестве автора можете указать ваш логин или email.

Инструкция createTable содержит название таблицы (tableName), описание таблицы (remarks) и набор колонок (columns). Для каждой колонки нужно обязательно указывать имя (name) и тип (type). При необходимости описание колонки также можно указывать через remarks (см. пример для поля url), а ограничения можно указать в constraints. Название типа указывается аналогично таковому названию в sql. Параметр autoIncrement позволяет создавать идентификаторы, значение которых увеличивается на 1 при добавлении каждой новой записи в таблицу. Флаг primaryKey позволяет указать, что именно это поле вы хотите сделать первичным ключом в таблице. Параметр primaryKeyName позволяет в явном виде задать имя данного ограничения. Делать это необязательно, но я настоятельно рекомендую давать имена для любых ограничений по определённым правилам.

Параметр nullable отвечает за допустимость значений null в данной колонке. Если nullable=false, то колонка в БД получит модификатор not null. По умолчанию nullable=true, т.е. null в ячейке допускается.

У каждой статьи должен быть уникальный url, по которому её можно найти в нашем блоге. Поэтому для url добавим специальное ограничение unique=true, а также дадим этому ограничению имя с помощью параметра uniqueConstraintName.

Статья может быть ещё не до конца написана, тогда её не следует отображать посетителям нашего блога. Поэтому добавим в таблицу поле is_visible и по умолчанию оно будет равно false. Значение по умолчанию позволяет задавать параметр defaultValue.

Наконец, у статьи есть время создания. По умолчанию при вставке будем проставлять текущее время. Для этого в качестве значения defaultValue укажем функцию now().

Теперь при запуске приложения liquibase создаст в нашей БД таблицу article, DDL которой приведён ниже:

create table article
(
    id         integer generated by default as identity
        constraint article_pk
            primary key,
    url        varchar(50)             not null
        constraint article_url_uq
            unique,
    title      varchar(50)             not null,
    is_visible boolean   default false not null,
    created    timestamp default now() not null,
    chapter_id integer                 not null
        constraint article_chapter_fk
            references chapter
);

comment on table article is 'Статьи';

comment on column article.url is 'Постоянная ссылка на статью';

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

  1. Создать таблицу-справочник разделов для статей (chapter)
  2. Заполнить эту таблицу нужными разделами
  3. Добавить связь таблицы article с таблицей chapter с помощью foreign key

Добавим в файл changelog.yml второй changeSet:

  - changeSet:
      id: DEV-2
      author: devmarkru
      changes:
        - createTable:
            tableName: chapter
            remarks: Разделы для статей
            columns:
              - column:
                  name: id
                  type: integer
                  constraints:
                    primaryKey: true
                    primaryKeyName: chapter_pk
                    nullable: false
              - column:
                  name: title
                  type: varchar(50)
                  constraints:
                    nullable: false

Для создания таблицы chapter используем уже знакомый нам тип изменений createTable. В этой таблице разделов будет всего два поля: id и title. Также определяем тут primary key, как и в первой таблице.

Вставка данных в таблицу

После создания этой таблицы нам нужно наполнить её названиями разделов. Для этого будем использовать инструкцию insert, которая на уровне БД будет преобразована в обычный insert into.

        - insert:
            tableName: chapter
            columns:
              - column:
                  name: id
                  value: 1
              - column:
                  name: title
                  value: Политика
        - insert:
            tableName: chapter
            columns:
              - column:
                  name: id
                  value: 2
              - column:
                  name: title
                  value: Культура

Для вставки данных мы указываем имя таблицы и колонки, для каждой из которых указывается её имя и значение. В данном примере две инструкции insert дадут нам две строки в таблице chapter.

Добавление новой колонки и внешнего ключа

Наконец, свяжем таблицу article с таблицей chapter через внешний ключ (foreign key).

        - addColumn:
            tableName: article
            columns:
              - column:
                  name: chapter_id
                  type: integer
                  constraints:
                    nullable: false
        - addForeignKeyConstraint:
            baseTableName: article
            baseColumnNames: chapter_id
            referencedTableName: chapter
            referencedColumnNames: id
            constraintName: article_chapter_fk

Инструкция addColumn добавляет новое поле chapter_id в таблицу article. Инструкция addForeignKeyConstraint вешает на это поле ограничение типа «внешний ключ». baseTableName - таблица, в которой находится внешний ключ. baseColumnNames - имя колонки для внешнего ключа. referencedTableName - имя таблицы, на которую ссылается таблица с внешним ключом. referencedColumnNames - имя колонки, на которую ссылается внешний ключ (чаще всего это первичный ключ id). Наконец, constraintName позволяет указать имя для внешнего ключа.

Теперь, если мы снова запустим приложение, то будет создана вторая таблица chapter. В неё будут вставлены две записи, а затем в таблицу article будет добавлен первичный ключ chapter_id.

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

В процессе описания изменений легко ошибиться или что-то забыть. В случае ошибки на каком-либо шаге все изменения в этом changeSet будут отменены, так что можно не опасаться за сломанную базу. Если же вы написали скрипт правильно, но про что-то забыли, то лучше отменить («откатить») изменения. Для этого просто аккуратно вручную отмените ВСЕ внесенные в данном changeSet изменения (например, удалите только что созданную таблицу или поле). Затем удалите соответствующую запись из таблицы databasechangelog, которая связана с этими изменениями. После этого вы можете повторно накатить изменения, как будто они накатываются в первый раз.

Выводы

Как вы сами убедились, liquibase позволяет в декларативном стиле описывать все изменения, которые вы хотите произвести со структурой вашей БД. Liquibase абстрагируется от синтаксиса конкретной СУБД и потому может работать с любой из них (главное предоставить нужный драйвер). При этом синтаксис выглядит несколько многословным, но вы можете выбрать тот формат, который вам больше всего подходит: xml, json или yaml.

Но самое главное преимущество liquibase - это автоматизация процесса наката изменений в БД. С увеличением количества экземпляров вашего приложения и серверов для них это становится особенно актуально.



Комментарии

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

×

devmark.ru