Skip to main content

Command Palette

Search for a command to run...

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

Published
7 min readView as Markdown
Y

My name is Yothin Muangsommuk. I've been in many roles in Software Development team, Software Engineer, Tech Lead, and Technical Coach. My focus has always been improving a sustainability and maintainability through Technical Excellence and Software Craftsmanship. I also A Python enthusiasm, An occasional conference speaker (PyCon APAC, Code Mania, Barcamp Bangkok), and a community organizer (PyCon Thailand, PySomTum, GDG Cloud Bangkok).

Interests: Programming Language Design, Software Engineering, Distributed System

Installation

  1. ลง Liquibase ใน pom.xml
<dependencies>
    <dependency>
        <groupId>org.liquibase</groupId>
        <artifactId>liquibase-core</artifactId>
        <version>3.6.2</version>
    </dependency>
</dependencies>
  1. เพิ่ม 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

90 views

More from this blog

ยิง request แบบไม่มีสะดุดด้วย emacs REST client

ช่วงปีใหม่ผมมีเวลาไปเรียนคอร์ส The Ultimate Go Series ของพี่ยอดซึ่งจากใจคนที่ดู Go อยู่ห่างๆ ผมบอกได้เลยว่าคอร์สนี้สอนได้ครบเครื่องมากในเวลาไม่กี่ชั่วโมง แต่นั่นไม่ใช่ประเด็นครับ ประเด็นคือตอนคอร์ส 2 ที่กำลังเรียนเรื่อง API พี่ยอดใช้ HTTP Client ที่ร...

Jan 9, 20223 min read102
ยิง request แบบไม่มีสะดุดด้วย emacs REST client

[Book Review] The Phoenix Project เพราะ IT Operation ก็ไม่ต่างกับ Production Plant

ผมรู้จัก The Phoenix Project มานานมากและมันวนเวียนมาเกือบจะได้อ่านหลายรอบ จนกระทั่งเมื่อประมาณกลางเดือนที่แล้ว ผมเพิ่งได้มีโอกาสจะหยิบมันขึ้นมาอ่าน และตั้งแต่ตอนนั้นผมแทบจะวางไม่ลงเลยตลอดระยะเวลาเกือบหนึ่งเดือนที่ผ่านมา และผมแนะนำให้กับทุกคนที่ทำงานใ...

Dec 20, 20214 min read456
[Book Review] The Phoenix Project เพราะ IT Operation ก็ไม่ต่างกับ Production Plant

Emacs diary EP2: สภาพแวดล้อม

ผมไม่รู้ว่าจะมีใครมาใช้ emacs แบบ เป็น text-editor ตัวแรกเลยมั้ย ผมไม่ใช่คนนึงแน่ๆ ซึ่งพอเราเปลี่ยนเครื่องมือ มันจะอารมณ์เดียวกับเราเปลี่ยนภาษา Programming ที่เราเขียนทุกวันเลย เพราะสิ่งแรกๆ ที่เราพยายามทำคือ "เราพยายามจะทำสิ่งเดียวกับที่เราเคยทำได้จ...

Dec 4, 20214 min read98
Emacs diary EP2: สภาพแวดล้อม

เรื่องประหลาดๆ ของการนับปีใน Java

ลืมหัวข้อ blog นี้ไปก่อนนะครับ แล้วลองพยายามเดาดูว่าโค้ด Java ข้างล่างนี้พยายามทำอะไร มันก็ดูตรงไปตรงมาใช่มั้ยครับ แล้วถ้ารันออกมาสมมติวันนี้วันที่ 1 ธันวาคม 2021 สิ่งที่ function นี้คืนกลับมาก็ เป็น 2020-12-01 ใช่มั้ยครับ ถ้าเราเขียน Unit test ไว้ ...

Dec 1, 20212 min read96
เรื่องประหลาดๆ ของการนับปีใน Java

Make Time | โลกมันวุ่นวาย แต่เรายังหาความสงบได้นะ

เคยมีความรู้สึกแบบนี้มั้ยครับ อารมณ์ประมาณว่า อยากจะเรียนคอร์สนั้นที่ซื้อไว้ตั้งนานแล้วให้จบ อยากจะอ่านหนังสือเล่มที่ซื้อเมื่อเดือนก่อน อยากจะทำ side project ที่คิดไว้ให้ออกมาเป็นรูปเป็นร่าง อยากจะมีทำให้ความสัมพันธ์ในชีวิตกับคนที่เราแคร์ดีขึ้น อะไรแ...

Nov 18, 20212 min read144
Make Time | โลกมันวุ่นวาย แต่เรายังหาความสงบได้นะ
P

Python by Cow

22 posts