При разработке и дальнейшей поддержки приложения база данных изменяется: добавляются, удаляются таблицы, столбцы и т.д. Для упрощения отслеживания изменений существует 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'ов, но доверять ему абсолютно невозможно. При простом добавлении и удалении колонок он справляется на ура. Но если нужно что-то сложнее, то лучше пройтись глазами по полученному файлу, и если требуется, модифицировать его.
