Автоматическое создание changeSet'ов Liquibase из Java entity

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

Каждый раз при добавление или изменение Entity, мы должны добавить новый changSet. Но что если я скажу, что есть плагин, который сам создает changeSetы на основе нашей Entity и уже существующей структуры базы данных?

Нам понадобится java, spring, gradle и liquibase plugin.

В примерах используется lombok, но можно и без него. СУБД - PostgreSQL.

Начальные данные

Для начала нужно создать проект и пару простых Entity.

Базовый класс:

import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import javax.persistence.*;
import java.time.OffsetDateTime;

@Data
@NoArgsConstructor
@MappedSuperclass
public class BaseEntity {

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

    @CreationTimestamp
    @Column(updatable = false, nullable = false)
    private OffsetDateTime createDate;

    @UpdateTimestamp
    @Column(nullable = false)
    private OffsetDateTime updateDate;
}

Класс для хозяина:

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Data
@NoArgsConstructor
@Entity
@Table(name = "person")
@EqualsAndHashCode(callSuper = true)
public class PersonEntity extends BaseEntity {

    @Column
    private String name;

    @OneToMany(mappedBy = "owner", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<AnimalEntity> animals = new ArrayList<>();
} 

Класс животное:

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Data
@NoArgsConstructor
@Entity
@Table(name = "animal")
@EqualsAndHashCode(callSuper = true)
public class AnimalEntity extends BaseEntity {

    @Column
    private String name;

    @Column
    private Long age;

    @Column(updatable = false, nullable = false)
    @Enumerated
    private AnimalType type;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(foreignKey = @ForeignKey(name = "animal_person_fk01"))
    private PersonEntity owner;
} 

И тип для жвотного:

public enum AnimalType {
    CAT,
    DOG,
    BIRD,
    HORSE
} 

После этого выполянем команду gradle build. Появляется директория build, в которой находяться наши классы.

Настройки плагина

Теперь в файле build.gradle добавляем плагин

plugins {
	id 'org.liquibase.gradle' version '2.0.'
}

Указываем директорияю для файла миграций, доступы к бд и ссылку на наши entity:

liquibase {
    activities {
        main {
            changeLogFile "$buildDir/generated-migrations.yaml" //указываем куда и с каким именем генерится файл
            //данные для доступа к бд
  					url "jdbc:postgresql://localhost:5432/testDb"
            username "test"
            password "test"
						//указываем путь к entity, а так же настройки для hibernate(диалект, сратегии наименования)
            referenceUrl 'hibernate:spring:entity?dialect=org.hibernate.dialect.PostgreSQL10Dialect&hibernate.physical_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy&hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy'
  					logLevel 'debug' //если хотим видить логи при выполнение команд
        }
        runList = "main"
    }
} 

Про настройки hibernate можно почитать тут, а про стратегии наименования есть пост на хабре.

Данные для доступа к бд можно вынести в отдельный файл и использовать как переменные:

 ext {
    database = new Properties().with {
        load(file("db.properties").newReader()) //название файла с данными для бд
        it
    }
}
liquibase {
    activities {
        main {
            changeLogFile "$buildDir/generated-migrations.yaml"
                      //используем переменные, а не сами значения
            url database.getProperty("dbUrl")
            username database.getProperty("dbUsername")
            password database.getProperty("dbPassword")
            referenceUrl 'hibernate:spring:entity?dialect=org.hibernate.dialect.PostgreSQL10Dialect&hibernate.physical_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy&hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy'
        }
        runList = "main"
    }
}

Сам файл с переменными db.properties

dbUrl=jdbc:postgresql://localhost:5432/testDb
dbUsername=test
dbPassword=test 

Для работы плагину нужно драйвера для дб, парсеры для ченджлога и т.д.

liquibaseRuntime 'org.liquibase:liquibase-core:3.8.9'
liquibaseRuntime("ch.qos.logback:logback-core:1.2.3")
liquibaseRuntime("ch.qos.logback:logback-classic:1.2.3")
//драйвер БД
liquibaseRuntime 'org.postgresql:postgresql'
//hibernate & spring & jpa
liquibaseRuntime 'org.liquibase.ext:liquibase-hibernate5:3.6'
liquibaseRuntime 'org.springframework.data:spring-data-jpa'
liquibaseRuntime 'org.springframework.boot:spring-boot'
liquibaseRuntime sourceSets.main.output
//для записи в yaml
liquibaseRuntime 'org.yaml:snakeyaml:1.26'

Обязательно нужно указать liquibaseRuntime sourceSets.main.output для того, чтобы плагин смог найти entity.

Поднимаем БД

Для генирации скриптов потребуется так же поднять БД. Я делаю это в докере, но можно и просто на свое пк.

version: "3.5"
services:

  db:
    ports:
      - 5432:5432
    image: postgres:12
    environment:
      POSTGRES_DB: testDb
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test 

Генерим changSet'ы

Для генерации выбираем команду diffChangeLog

В директории билд у нас окажется файл changelog.yaml с автоматически созданными changSet'ами:

Пример сгенерированного файла
databaseChangeLog:
  - changeSet:
      id: 1638648715035-1
      author: AnnKont (generated)
      changes:
        - createTable:
            columns:
              - column:
                  autoIncrement: true
                  constraints:
                    nullable: false
                    primaryKey: true
                    primaryKeyName: animalPK
                  name: id
                  type: BIGINT
              - column:
                  constraints:
                    nullable: false
                  name: create_date
                  type: TIMESTAMP WITHOUT TIME ZONE
              - column:
                  constraints:
                    nullable: false
                  name: update_date
                  type: TIMESTAMP WITHOUT TIME ZONE
              - column:
                  name: age
                  type: BIGINT
              - column:
                  name: name
                  type: VARCHAR(255)
              - column:
                  constraints:
                    nullable: false
                  name: type
                  type: INTEGER
              - column:
                  name: owner_id
                  type: BIGINT
            tableName: animal
  - changeSet:
      id: 1638648715035-2
      author: AnnKont (generated)
      changes:
        - createTable:
            columns:
              - column:
                  autoIncrement: true
                  constraints:
                    nullable: false
                    primaryKey: true
                    primaryKeyName: personPK
                  name: id
                  type: BIGINT
              - column:
                  constraints:
                    nullable: false
                  name: create_date
                  type: TIMESTAMP WITHOUT TIME ZONE
              - column:
                  constraints:
                    nullable: false
                  name: update_date
                  type: TIMESTAMP WITHOUT TIME ZONE
              - column:
                  name: name
                  type: VARCHAR(255)
            tableName: person
  - changeSet:
      id: 1638648715035-3
      author: AnnKont (generated)
      changes:
        - addForeignKeyConstraint:
            baseColumnNames: owner_id
            baseTableName: animal
            constraintName: animal_person_fk01
            deferrable: false
            initiallyDeferred: false
            referencedColumnNames: id
            referencedTableName: person
            validate: true

Теперь копируем этот файл в ресурсы в директорию db.changelog и можем запускать наше приложение.

После запуска будут созданы таблицы. Если нам нужно что-то изменить, смело меняем все в entity. И обязательно делаем clean и build.

public class PersonEntity extends BaseEntity {
		//заменили name на firstName
    @Column
    private String firstName;
		//добавили новую колонку
    @Column
    private String secondName;
 		...
} 
public class AnimalEntity extends BaseEntity {
		//добавили ограничение
    @Column(nullable = false)
    private String name;
		...
}

Снова выполняем команду diffChangeLog и получаем новые changSet'ы

Пример сгенерированного файла
databaseChangeLog:
  - changeSet:
      id: 1638650678646-1
      author: AnnKont (generated)
      changes:
        - addNotNullConstraint:
            columnDataType: varchar(255)
            columnName: name
            tableName: animal
            validate: true
  - changeSet:
      id: 1638650678646-2
      author: AnnKont (generated)
      changes:
        - addColumn:
            columns:
              - column:
                  name: first_name
                  type: varchar(255)
            tableName: person
  - changeSet:
      id: 1638650678646-3
      author: AnnKont (generated)
      changes:
        - addColumn:
            columns:
              - column:
                  name: second_name
                  type: varchar(255)
            tableName: person
  - changeSet:
      id: 1638650678646-4
      author: AnnKont (generated)
      changes:
        - dropColumn:
            columnName: name
            tableName: person

Но если вам понадобится прежде чем удалить столбец перенести из него данные, например, столбец переместили из одной таблицы в другую, то тут придется написать скрипт руками.
Так же как можно заметить, замена name на firstName != изменению имени столбца в сгенерированном файле. Плагин думает, что нужно полностью удалить столбец name и новый столбец firstName.

Посмотреть готовый проект можно на github.

Заключение

Liquibase plugin вполне может облегчить создание changLog'ов, но доверять ему абсолютно невозможно. При простом добавление и удаление колонок он справляется на ура. Но если нужно что-то сложнее, то лучше пройтись глазами по полученному файлу и если требуется, модифицировать его.

Источник: https://habr.com/ru/post/589205/


Интересные статьи

Интересные статьи

Большая часть приложений, которые мне встречались, хранят данные в SQL базе данных. Если у вас корпоративное приложение, то скорее всего имеется несколько стендов: стен...
Если мы хотим передать данные из Django в JavaScript, мы обычно говорим об API, сериализаторах, вызовах JSON и AJAX. Обычно дело усложняется наличием React или Angular на...
В этой статье мы говорим о переменных и связанных с ними темами!Научимся создавать переменные и константы.Узнаем что такое инициализация переменных.Поговорим о литералах....
Известный, но не очень популярный способ внедрения зависимостей. Попытка реализовать этот способ в популярных DI npm пакетах. Еще один свой DI. Читать дальше → ...
В процессе работы над своей игрой, которая повторяет некоторые элементы старых игр, я задумался, а так ли необходимо намеренно придерживаться ретро-стиля. И за что мы, собственно, любим старые ...