บันทึกวิธีใช้ liquibase ทำ database migration ให้ Spring boot
Installation
- ลง Liquibase ใน pom.xml
<dependencies>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>3.6.2</version>
</dependency>
</dependencies>
เพิ่ม configuration ให้ liquibase ใน application.properties
## Liquibase
spring.liquibase.enabled=true
spring.liquibase.parameters.EnumDataType=ENUM('VALUE1', 'VALUE2', 'VALUE3')
spring.liquibase.contexts=local
ตัวสำคัญคือ spring.liquibase.contexts
ตัวนี้เหมือนคอยบอกว่าเรารัน Spring โหมดอะไรอยู่ แล้วทำให้เราใช้ connection database ที่เราเซตไว้ใน spring ได้ด้วย
อีกจุดนึงคือ spring.liquibase.parameters
อันนี้เป็น custom parameter ที่เราเอาไว้ใช้ใน Changeset ซึ่งบางครั้งของ database คนละแบบจะต่างกันอย่างเช่น type ENUM ของ H2 เก็บเป็น enum ได้เลย แต่ PostgreSQL ตัว spring จะแปลงเป็น INT เลยต้องมาประกาศไว้ด้วย
Usage
- เพิ่มไฟล์ ChangeLog master ไฟล์นี้เป็นเหมือนไฟล์หลักไว้บอกว่ามี migration step อะไรบ้างผ่าน property ชื่อ
include.file
ไฟล์กับชื่อจะอยู่ที่/resources/db/changelog/db.changelog-master.yaml
ตัวนี้สามารถตั้งค่าให้ไปอยู่ที่อื่น ชื่ออื่นได้ แต่อันนี้คือชื่อ default หน้าตาของไฟล์จะประมาณ
databaseChangeLog:
- include:
file: db/changelog/001-add-user-table.yaml
- สร้าง ChangeSet ตัวนี้ไว้บอกว่าใน migration นี้มี change อะไรเกี่ยวกับ database บ้าง ตัวอย่างเช่นของ user
databaseChangeLog:
- changeSet:
id: 001-add-user-table
author: man
changes:
- createTable:
columns:
- column:
constraints:
nullable: false
primaryKey: true
primaryKeyName: user_pkey
name: id
type: UUID
- column:
constraints:
nullable: false
name: name
type: VARCHAR(250)
- column:
constraints:
nullable: false
name: type
type: ${EnumDataType}
- column:
name: is_admin
type: BOOLEAN
- column:
name: created_at
type: TIMESTAMP
tableName: user
ใน changeSet ต้องบอกว่ามันเป็น changeSet ซึ่งมี property หลักๆ ที่ใช้คือ
- id: อันนี้เอาไว้เป็น identifier ไม่ unique ก็ได้ลองแล้วติด แต่ best practice คือจะรันเลขหรือรันเป็นชื่อก็ได้ นี่เลยเอาทั้งสองอย่างแล้วเอาให้เป็นชื่อเดียวกับไฟล์ของ changeset เลยจะได้หาง่ายๆ
- author: ก็คือใครสร้าง changeset นี้
- changes: อันนี้เป็นตัวบอกว่าเราจะทำอะไรเกี่ยวกับ database ในตัวอย่างข้างล่างคือ createTable
โน๊ตนิดหน่อยเกี่ยวกับ createTable คือมันต้องการ key หลักสองอย่างคือ columns, tableName ก็ตามตัวเลย แต่ใน columns จะมีรายละเอียดนิดหน่อย
- column:
constraints:
nullable: false
primaryKey: true
primaryKeyName: user_pkey
name: id
type: UUID
ใน column required หลักๆ คือ name กับ type ซึ่งตามที่ใช้ใน SQL ที่สร้างของ database ตัวนั้นเลย ส่วน constraints เป็น optional
ในตัวอย่างจะเห็น type: ${EnumDataType}
อันนี้จะเอามาจากที่เราประกาศไว้ใน spring.liquibase.parameters
ใน application.properties มาใส่ให้
- addUniqueConstraint:
constraintName: uq_admin_name_user
columnNames: name, is_admin
tableName: user
อีกตัวอย่างเป็นการสร้าง unique constraints ของ table อันนี้ใช้คำสั่ง addUniqueConstraint
ในเลเวลเดียวกับ createTable
เลย ตรงนี้ required แค่ tableName กับ columnNames ส่วนชื่อถ้าไม่ใส่มันจะเรียงๆ ให้จาก properties ที่เราใส่ไป แนะนำให้ใส่ดีกว่า
How it work
เวลาที่ Spring ทำงาน มันจะสั่งรัน liquibase update ให้อัตโนมัติซึ่ง ถ้ายังไม่เคยมี liquibase ทำครั้งแรกมันจะสร้าง table ไว้เก็บ migration step หรือ changelog ด้วยหน้าตาแบบนี้
exampledb> select * from databasechangelog;
+-------------------------------------+----------+-------------------------------------------------------+----------------------------+-----------------+------------+------------------------------------+-------------------------------------------------------------------
| id | author | filename | dateexecuted | orderexecuted | exectype | md5sum | description
|-------------------------------------+----------+-------------------------------------------------------+----------------------------+-----------------+------------+------------------------------------+-------------------------------------------------------------------
| 001-add-user-table | man | db/changelog/001-add-user-table.yaml | 2020-10-27 10:12:27.875734 | 1 | EXECUTED | 8:09d73c59f70e73a65420a4eb3f044bc8 | createTable tableName=user
+-------------------------------------+----------+-------------------------------------------------------+----------------------------+-----------------+------------+------------------------------------+-------------------------------------------------------------------
สำคัญอย่างนึงคือตัว changeset ถ้าถูก execute ไปแล้วห้ามกลับไปแก้เด็ดขาด เพาะ md5sum มันจะไม่ตรงกัน แล้วมันจะไม่ยอม migrate ให้ ถึงแม้ว่า liquibase-maven-plugin จะสั่ง clear checksum ได้ แต่ดีที่สุดคืออย่าไปยุ่งกับมันเลย
Unit test setup
ถ้าเรา setup เท่าข้างบนแล้วไปรัน unit test เลย สิ่งที่เกิดขึ้นคือ
java.lang.IllegalStateException: Failed to load ApplicationContext
...
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'liquibase' defined in class path resource [org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration$LiquibaseConfiguration.class]: Invocation of init method failed; nested exception is java.lang.ArrayIndexOutOfBoundsException: 1
ซึ่งหาเท่าไรใน Stackoverflow ก็หาไม่เจอ (ลองแล้วทุก link) แต่วิธีแก้คือให้ไปเพิ่ม liquibase property ใน application.properties
ของเทสก็จะได้ละ
spring.liquibase.enabled=true
spring.liquibase.parameters.EnumDataType=ENUM('VALUE1', 'VALUE2', 'VALUE3')
Further reading
จริงๆ อยากให้มันสร้าง changeLog จาก Entity ได้เลยแต่ยังหาวิธีไม่ได้ แล้วก็ liquibase-maven-plugin ใช้กับ intelliJ ก็ยังเอ๋อๆ อยู่
อีกอย่างคือ ในตัวอย่างที่ยกมามีแต่ forward migration ยังไม่มี rollback ซึ่งจริงๆ มัน support rollback นะ แล้วก็เขียนอยู่ใน changeSet นะแหละ แต่ยังไม่ได้ดูเพิ่ม
Note
Liquibase Maven plugin
เอาไว้สั่ง liquibase แบบไม่ต้อง start Spring แต่ต้องลง dependencies ของมันเองด้วย
<dependencies>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<version>3.6.2</version>
</dependency>
</dependencies>
...
<build>
<plugins>
<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<version>3.6.2</version>
<configuration>
<propertyFile>src/main/resources/liquibase.properties</propertyFile>
</configuration>
</plugin>
</plugins>
</build>
liquibase.properties
อันนี้เอาไว้ให้ liquibase-maven-plugin
คุยกับ database ได้โดยไม่ต้องง้อ Spring เพราะปกติ Spring จะเป็นคนรัน Liquibase ให้อยู่แล้ว
changeLogFile=src/main/resources/db/changelog/db.changelog-master.yaml
url=jdbc:postgresql://localhost:5432/exampledb
driver=org.postgresql.Driver
username=user
password=password
Reference
- ถ้า setup ง่ายๆ ดูตามนี้ได้ แต่ไฟล์ changelog, changeset เค้าใช้เป็น XML ซึ่งอ่านยากกว่า YAML https://www.baeldung.com/liquibase-refactor-schema-of-java-app
- วิธีเพิ่ม unique constraint ให้ table https://docs.liquibase.com/change-types/community/add-unique-constraint.html
- อันนี้ช่วยหาคำตอบตอน Spring context, profile หาไม่เจอกับ liquibase https://stackoverflow.com/questions/40088915/using-spring-boot-profiles-with-liquibase-changeset-context-attribute-to-manage
- อันนี้ช่วยหาคำตอบตอนหาไฟล์ master changelog ไม่เจอ https://stackoverflow.com/questions/41990295/java-illegalstateexception-cannot-find-changelog-location-class-path-resourc
- ตัวอย่างนี้ดีเพราะใช้ YAML แล้วก็รู้เรื่อง liquibase.parameters จากที่นี่แหละ https://reflectoring.io/database-migration-spring-boot-liquibase/
- อันนี้ช่วยหาคำตอบให้ตอนงงๆ liquibase.properties ว่าตกลงใช้ไม่ใช้ https://watson.earth/2018/09/21/combining-spring-boot-and-liquibase/
- อันนี้ดูค้างไว้เรื่อง Rollback, pre-condition ของ changeset ว่าใช้ยังไง https://docs.liquibase.com/concepts/advanced/preconditions.html
- อันนี้ดีมาก เป็น Best Practice ของ Liquibase เองเลย หลายๆ อย่างได้มาจากอันนี้แหละ https://www.liquibase.org/get-started/best-practices
- อันนี้พูดถึง liquibase-maven-plugin โดยเฉพาะ https://docs.liquibase.com/tools-integrations/maven/home.html