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