“database schema change management solution”
마이그레이션 스크립트를 작성하여 DB 스키마의 형상관리를 지원해주는 툴이다.
Liquibase is a database schema change management solution that enables you to revise and release database changes faster and safer from development to production.
To start using Liquibase quickly and easily, you can write your migration scripts in SQL.
To take advantage of database abstraction abilities that allow you to write changes once and deploy to different database platforms, you can specify database-agnostic changes in XML, JSON, or YAML.
Flyway 라는 좋은 친구도 있다.
하지만 롤백 지원, RDS 로그 테이블 저장, 테이블 간 DIFF 스냅샷 지원 등등 다양한 기능이 지원되는 게 좋았다.
또한 코드 상으로 버전관리가 되는 부분이 이점으로 다가왔다.
liquibase 를 통해 ddl 을 날릴 수 있다. 이렇게 처리된 내역은 DATABASECHANGELOG 라는 RDB 상에 박혀있게된다.
또한 git 에도 남게되어 형상관리하기 용이하다.
( 또 친히 여러 군데에서 한 번에 스키마 변경하지 말라고 DATABASECHANGELOGLOCK 테이블을 통해 LOG 테이블에 대한 LOCK 을 지원한다,,, 스윗,,,, )
각 팀이 편한 방식을 따르면 될 것 같다. 필자의 경우 ddl 을 사용하는 게 편해서 sql 을 선택했다. (yml 도 지원하는데 이건 포맷을 따라야하므로 적응기가 필요해보인다.)
아래 그림과 같이 source 와 target 을 통해 log 를 저장할 수 있다.
여러 스테이징을 관리하고 있다면 참 유용한 기능인 것 같다.
JPA Buddy 와 IntelliJ 내장 기능을 통해 처리가능함
*flyway 는 유료소스에서만 가능하다. (가능은 한데,,, undo 스크립트를 따로 작성해 실행하도록 해야한다,,)
*또한 flyway 는 데이터에 대한 스냅샷을 지원치 않는다.
https://www.jetbrains.com/help/idea/liquibase.html#generate-migration-script
현재 모델에 대한 스냅샷 또한 코드로써 저장 가능함
Liquibase Docker container image includes the Liquibase software, Java, JDBC drivers, and all other dependencies already preconfigured. The image is based on the Eclipse Temurin image
eclipse-temurin:17-jre-focal
.Docker pull command:
docker pull liquibase/liquibase
changelog 를 건드리거나 이전 작업물을 조금이라도 건드리면 에러를 뱉는다.
물론 스키마를 자주 변경하지는 않겠지만, 디버깅에도 걸리지 않아서 이게 어지간히 빡친다.
liquibase 는 comment 도 중요데이터이므로 빼먹지 말자. new line 도 신경써줘야한다.
가령 아래와 같이 --liquibase formatted sql
코멘트가 빠지면 추가되는 changeset 에 대해서 checksum 에러를 내뱉는다.
--liquibase formatted sql
--changeset sql-test:1
--validCheckSum: 8:b4fd16a20425fe377b00d81df722d604
create table test2(
id int
);
https://docs.liquibase.com/concepts/changelogs/changeset-checksums.html#sql_example
https://www.liquibase.com/blog/what-affects-changeset-checksums
--validCheckSum
을 추가하여 허용되는 checksum 이야~ 이 친구는 안전하다구~
라고 알려준다.
runOnChange
속성을 추가한다.
위 세 가지 방법을 다 처리하더라도 안 되면 DB 롤백을 해줘야한다. (이게 진짜 고역이다,,)
웬만한 이슈는 1,2 번이였다. 필자는 1,2까지만 해줘도 다 해결되었었다.
대부분 포맷을 잘 못 맞추었거나, syntax 에러가 발생하거나였다.
mysql server 에 들어가서 liquibase 에 대한 command 를 날려줘야한다.
즉, 이러한 경우 spring boot 에 command 를 날릴 수 없다
(왜 이런 건 사용자 친화적이 아닌 만드는 사람 친화적으로 만드는지 모르겠다,,)
따라서 cli 를 적용하는 bean 을 만들어 한 번 처리를 해줘야하는 단점이 존재한다.
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.2'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'io.dodn.demo'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
// runtimeOnly 'io.asyncer:r2dbc-mysql'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.liquibase:liquibase-core'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
application.yml
spring:
# config:
# activate:
# on-profile: local
# DB 연결
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3307/liquibase-practice-mysql?useSSL=false&useUnicode=true
username: admin
password: 1234qwer!
# JPA 설정
jpa:
database-platform: org.hibernate.dialect.MySQLDialect
properties:
hibernate:
jdbc:
time_zone: Asia/Seoul
batch_size: 1000
order_inserts: true
order_updates: true
ddl-auto: create # ⚠️ 절대 none 으로 유지할 것 ⚠️
auto_quote_keyword: true # 예약어 사용가능
globally_quoted_identifiers: true # 예약어 사용가능
show_sql: true # sql 로깅
generate_statistics: true # 쿼리수행 통계
format_sql: true # SQL문 정렬하여 출력
highlight_sql: true # SQL문 색 부여
default_batch_fetch_size: 1000
open-in-view: false
# Liquibase 설정
liquibase:
change-log: classpath:db/changelog/db.changelog-master.sql
logging:
pattern:
dateformat: yyyy-MM-dd HH:mm:ss.SSS,Asia/Seoul
level:
root: info
com.restApi.restApiSpringBootApp: debug
org.springframework: WARN
org:
springframework:
web:
reactive:
function:
client: DEBUG
hibernate:
orm:
jdbc:
bind: TRACE # jdbc bind value 로깅 처리
BaseEntity.java
@Getter
@Setter
@MappedSuperclass
@SuperBuilder
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor
public abstract class BaseTimeEntity {
@CreationTimestamp
@Column(updatable = false)
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime createdAt;
@UpdateTimestamp
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime updatedAt;
}
User.java
@Entity
@Getter
@Builder
@DynamicUpdate
@DynamicInsert
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "liquibase_user")
public class User extends BaseTimeEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_idx", nullable = false, updatable = false)
private Long userIdx;
@Column(name = "user_name", length = 16)
private String userName;
@Column(name = "user_email", length = 64)
private String userEmail;
@Column(name = "user_password", length = 255)
private String userPassword;
}
db.changelog-master.sql
--liquibase formatted sql
--changeset admin:sample1_1
ALTER TABLE liquibase_user DROP user_addr
--changeset admin:sample1_2
ALTER TABLE liquibase_user ADD user_addr VARCHAR(255);
이에 따라 DATABASECHANGELOG 테이블에 아래와 같이 저장되게 된다.
build.gradle
import org.liquibase.gradle.LiquibaseTask
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.2'
id 'io.spring.dependency-management' version '1.1.6'
id 'org.liquibase.gradle' version '2.2.0'
}
group = 'io.dodn.demo'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
liquibaseRuntime.extendsFrom runtimeClasspath
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
// runtimeOnly 'io.asyncer:r2dbc-mysql'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.liquibase:liquibase-core'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
liquibaseRuntime sourceSets.main.output
liquibaseRuntime 'info.picocli:picocli:4.6.1'
}
tasks.named('test') {
useJUnitPlatform()
}
liquibase {
activities {
main {
changeLogFile "src/main/resources/db/changelog/db.changelog-master.sql"
url "jdbc:mysql://localhost:3307/liquibase-practice-mysql"
username "admin"
password "1234qwer!"
}
}
runList = 'main'
}
tasks.register('liquibaseSnapshot', LiquibaseTask) {
changeLogFile 'src/main/resources/db/changelog/db.changelog-master.sql'
url 'jdbc:mysql://localhost:3307/liquibase-practice-mysql'
username 'admin'
password '1234qwer!'
}
위와 같이 작성한 뒤, ./gradlew snpashot 을 실행해보자.
아래와 같이 나오는 게 보일 것이다.
AM 2:56:54: Executing 'snapshot'...
> Task :snapshot
liquibase-plugin: The 'changeLogFile' has been deprecated. Please use 'changelogFile' in your activity instead.
liquibase-plugin: Running the 'main' activity...
####################################################
## _ _ _ _ ##
## | | (_) (_) | ##
## | | _ __ _ _ _ _| |__ __ _ ___ ___ ##
## | | | |/ _` | | | | | '_ \ / _` / __|/ _ \ ##
## | |___| | (_| | |_| | | |_) | (_| \__ \ __/ ##
## \_____/_|\__, |\__,_|_|_.__/ \__,_|___/\___| ##
## | | ##
## |_| ##
## ##
## Get documentation at docs.liquibase.com ##
## Get certified courses at learn.liquibase.com ##
## ##
####################################################
Starting Liquibase at 02:56:56 (version 4.27.0 #1525 built at 2024-03-25 17:08+0000)
[2024-08-05 02:56:56] INFO [liquibase.ui] ####################################################
## _ _ _ _ ##
## | | (_) (_) | ##
## | | _ __ _ _ _ _| |__ __ _ ___ ___ ##
## | | | |/ _` | | | | | '_ \ / _` / __|/ _ \ ##
## | |___| | (_| | |_| | | |_) | (_| \__ \ __/ ##
## \_____/_|\__, |\__,_|_|_.__/ \__,_|___/\___| ##
## | | ##
## |_| ##
## ##
## Get documentation at docs.liquibase.com ##
## Get certified courses at learn.liquibase.com ##
## ##
####################################################
Starting Liquibase at 02:56:56 (version 4.27.0 #1525 built at 2024-03-25 17:08+0000)
Liquibase Version: 4.27.0
[2024-08-05 02:56:56] INFO [liquibase.ui] Liquibase Version: 4.27.0
WARNING: License service not loaded, cannot determine Liquibase Pro license status. Please consider re-installing Liquibase to include all dependencies. Continuing operation without Pro license.
[2024-08-05 02:56:56] INFO [liquibase.ui] WARNING: License service not loaded, cannot determine Liquibase Pro license status. Please consider re-installing Liquibase to include all dependencies. Continuing operation without Pro license.
[2024-08-05 02:56:56] INFO [liquibase.integration] Starting command execution.
[2024-08-05 02:56:57] INFO [liquibase.snapshot] Creating snapshot
Database snapshot for jdbc:mysql://localhost:3307/liquibase-practice-mysql
-----------------------------------------------------------------
Database type: MySQL
Database version: 9.0.1
Database user: admin@172.18.0.1
Included types:
liquibase.structure.core.Catalog
liquibase.structure.core.Column
liquibase.structure.core.ForeignKey
liquibase.structure.core.Index
liquibase.structure.core.PrimaryKey
liquibase.structure.core.Schema
liquibase.structure.core.Sequence
liquibase.structure.core.Table
liquibase.structure.core.UniqueConstraint
liquibase.structure.core.View
Catalog: liquibase-practice-mysql
liquibase.structure.core.Index:
PRIMARY
columns:
ID
table: DATABASECHANGELOGLOCK
unique: true
PRIMARY
columns:
user_idx
table: liquibase_user
unique: true
liquibase.structure.core.PrimaryKey:
PRIMARY
backingIndex: PRIMARY
columns:
ID
table: DATABASECHANGELOGLOCK
PRIMARY
backingIndex: PRIMARY
columns:
user_idx
table: liquibase_user
liquibase.structure.core.Table:
DATABASECHANGELOG
columns:
AUTHOR
nullable: false
order: 2
type: VARCHAR(255 BYTE)
COMMENTS
nullable: true
order: 9
type: VARCHAR(255 BYTE)
CONTEXTS
nullable: true
order: 12
type: VARCHAR(255 BYTE)
DATEEXECUTED
nullable: false
order: 4
type: DATETIME
DEPLOYMENT_ID
nullable: true
order: 14
type: VARCHAR(10 BYTE)
DESCRIPTION
nullable: true
order: 8
type: VARCHAR(255 BYTE)
EXECTYPE
nullable: false
order: 6
type: VARCHAR(10 BYTE)
FILENAME
nullable: false
order: 3
type: VARCHAR(255 BYTE)
ID
nullable: false
order: 1
type: VARCHAR(255 BYTE)
LABELS
nullable: true
order: 13
type: VARCHAR(255 BYTE)
LIQUIBASE
nullable: true
order: 11
type: VARCHAR(20 BYTE)
MD5SUM
nullable: true
order: 7
type: VARCHAR(35 BYTE)
ORDEREXECUTED
nullable: false
order: 5
type: INT(10)
TAG
nullable: true
order: 10
type: VARCHAR(255 BYTE)
default_tablespace: false
DATABASECHANGELOGLOCK
columns:
ID
nullable: false
order: 1
type: INT(10)
LOCKED
nullable: false
order: 2
type: TINYINT(3)
LOCKEDBY
nullable: true
order: 4
type: VARCHAR(255 BYTE)
LOCKGRANTED
nullable: true
order: 3
type: DATETIME
default_tablespace: false
indexes:
PRIMARY
columns:
ID
unique: true
primaryKey: PRIMARY
backingIndex: PRIMARY
columns:
ID
liquibase_user
columns:
created_at
nullable: true
order: 2
type: DATETIME
updated_at
nullable: true
order: 3
type: DATETIME
user_addr
nullable: true
order: 7
type: VARCHAR(255 BYTE)
user_email
nullable: true
order: 5
type: VARCHAR(64 BYTE)
user_idx
autoIncrementInformation: GENERATED null AUTO INCREMENT START WITH 1 INCREMENT BY 1
nullable: false
order: 1
type: BIGINT(19)
user_name
nullable: true
order: 4
type: VARCHAR(16 BYTE)
user_password
nullable: true
order: 6
type: VARCHAR(255 BYTE)
default_tablespace: false
indexes:
PRIMARY
columns:
user_idx
unique: true
primaryKey: PRIMARY
backingIndex: PRIMARY
columns:
user_idx
[2024-08-05 02:56:58] INFO [liquibase.command] Command execution complete
Liquibase command 'snapshot' was executed successfully.
[2024-08-05 02:56:58] INFO [liquibase.ui] Liquibase command 'snapshot' was executed successfully.
BUILD SUCCESSFUL in 3s
1 actionable task: 1 executed
AM 2:56:58: Execution finished 'snapshot'.
그런데 하나 의문이 생겼다.
이에 대해 추가공부가 필요해보인다 흑흑,,,
Bytebase vs. Liquibase: a side-by-side comparison for database schema migration
다만 bytebase 도 단점이 보인다.