บันทึกวิธีใช้ liquibase ทำ database migration ให้ Spring boot

บันทึกวิธีใช้ liquibase ทำ database migration ให้ Spring boot
Photo by Jan Antonin Kolar / Unsplash

Installation

  1. ลง 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

  1. เพิ่มไฟล์ 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
  1. สร้าง 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