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

от автора

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

Каждый раз при добавлении или изменении Entity мы должны добавить новый changeSet. Но что, если я скажу, что есть плагин, который сам создает 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) @ToString(callSuper = true) public class PersonEntity extends BaseEntity {      @Column     private String name;      @OneToMany(mappedBy = "owner", cascade = CascadeType.ALL, orphanRemoval = true)   @EqualsAndHashCode.Exclude     @ToString.Exclude     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) @ToString(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")) @EqualsAndHashCode.Exclude     @ToString.Exclude     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 

Генерим changeSet’ы

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

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

Пример сгенерированного файла
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 и получаем новые changeSet’ы

Пример сгенерированного файла
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/articles/589205/


Комментарии

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

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