Picture of the author
Published on

ใช้ Liquibase กับ Spring Boot

Liquibase Spring Boot

ขอบคุณรูปภาพจาก javadeveloperzone.com

ก่อนจะเริ่มต้น อย่างน้อยควรจะมีพื้นฐานและเคยใช้ Sprig Boot , SQL มาบ้างไม่มากก็น้อย

สำหรับคนที่ขี้เกียจอ่านทั้งหมด 😂


Liquibase คืออะไร ?

เป็นเครื่องมือ Open Source ที่ช่วยจัดการ Database Schema โดยจะยึดตามการเปลี่ยนแปลงของโครงสร้างต่างๆของตาราง (Table,Column,View,Sequence) หรือแม้แต่การย้ายฐานข้อมูล เปรียบเสมือน Tool ที่คอยตรวจสอบและดูแลทุกอย่างใน Database ให้เป็นปัจจุบันเสมอในทุก Environment อยู่ในรูปแบบของ XML, YAML หรือ JSON โดย Syntax ไม่ได้ยึดตาม Database Platform ที่ใช้ สามารถ Custom เพิ่มเติมได้ด้วยการเขียน SQL Script รองรับทั้ง MySQL, PostgreSQL, Oracle, SQL Server และอื่นๆ

. . .

ทำไมต้องใช้ Liquibase ?

Database Schemas จะเป็นปัจจุบันและเหมือนกันทุก Environment โดยไม่ต้องมาคอยดูแลและจัดการ Script ทุกครั้งด้วยตัวเอง (ลด Human Error) รวมถึงการย้าย Server , Environment หรือเปลี่ยน Database ก็สามารถทำได้ง่ายขึ้น

. . .

ถ้าไม่ใช้ Liquibase ต้องทำไรบ้าง ?

  1. หลังจาก Run script SQL บน Dev แล้วต้องมีการจัดเก็บ Script ส่วนนี้ไว้ทุกครั้ง โดยต้องมีที่จัดเก็บให้เป็นระเบียบ และมีโอกาสที่ Developer จะลืมหรือตกหล่นไปบาง Script ด้วย
  2. จากข้อ 1. ที่พูดถึงการจัดเก็บ Script บางครั้ง ถ้า Script นั้น Developer ไม่ได้เป็นคนเขียน แต่เป็น BA , SA หรือตำแหน่งอื่นๆ Script นั้นอาจจะไม่ได้ถูกจัดเก็บไว้ใน Project ซึ่งที่ถูกต้องควรจะอยู่ใน Project นั้นๆ
  3. หลังจาก Deploy แล้วต้องนำ Script ที่เตรียมไว้มา Run อีกรอบบน Environment นั้นๆ ขั้นตอนนี้มีโอกาสที่จะนำ Script มา Run ไม่ครบ หรือ Run ผิด Sequence อาจจะทำให้เกิดปัญหา Deploy ไม่ผ่าน

ลองคิดเล่นๆดูนะครับ ถ้าการ Deploy ในรอบนั้น มีอยู่ประมาณ 10 Script และคนที่ Deploy ไม่ใช่ Developer เจ้าของ Script มีหน้าที่แค่เอาทุกอย่างที่เตรียมไว้ไป Deploy ตาม Check List เมื่อเกิดปัญหาขึ้นมา คงจะเสียเวลาและวุ่นวายน่าดู โดยเฉพาะการ Deploy ที่ Production บางทีอาจจะมีเวลาที่จำกัด

ถ้าเราใช้ Liquibase ปัญหาหรือความเสี่ยงต่างๆที่กล่าวมานี้ จะไม่เกิดขึ้น เพราะถ้าเกิดปัญหา จะรู้และไม่ผ่านตั้งแต่ตอน Dev แล้ว ลดปัญหาที่อาจเกิดจาก Human error นั่นเอง

. . .

เพิ่ม Maven Dependency

เพิ่ม spring-boot-starter-jdbc , liquibase-core ที่ไฟล์ pom.xml และ Reimport maven

<dependencies>
    ...
    <dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-jdbc</artifactId>
	</dependency>
    <dependency>
        <groupId>org.liquibase</groupId>
        <artifactId>liquibase-core</artifactId>
    </dependency>
    ...
</dependencies>
. . .

เพิ่ม config ใน application.properties

#เป็นการกำหนด Path file ของ Db changelog ตั้งต้น 
#ถ้าเราไม่กำหนด Spring boot จะมองหา Path file 'db/changelog/db.changelog-master.yaml' เป็น Path ตั้งต้น
spring.liquibase.change-log = classpath:/db/db-changelog-master.xml

#ตั้งค่า Level logs ได้ ผมใช้เป็น INFO
logging.level.liquibase = INFO
. . .

Structure

└── src
    └── main
        └── resources
            └── db
                └── changelogs
                │   ├── table
                │   │   └── xxx01.xml
                │   │   └── xxx02.xml
                │   └── view
                │       └── v_xxx01.xml
                │       └── v_xxx02.xml
                └── sql
                │   └── init_master_xxx01.sql
                │   └── init_master_xxx02.sql
                └── db-changelog-master.xml

โครงสร้างในการสร้างโฟลเดอร์และไฟล์ต่างๆของ Liquibase จะใช้เป็นประมาณนี้ โดยจะแยกออกเป็นกลุ่มหลักๆ คือ

  • changelogs/table - จะเป็น Script ทั้งหมดที่เกี่ยวข้องกับตารางต่างๆ โดยจะแยกออกตามตาราง
  • changelogs/view - จะเป็น Script ทั้งหมดที่เกี่ยวข้องกับ View โดยจะแยกออกตาม View
  • sql - จัดเก็บ Script ในกรณีมีความซับซ้อนสูง ไม่สามารถเขียนเป็น Syntax ของ Liquibase ได้ หรือ สำหรับ Master data ต่างๆ

ถ้ามีอย่างอื่นนอกเหนือจาก table , view ก็สร้างเพิ่มใน changelogs

. . .

db-changelog-master.xml

สร้างไฟล์ใน Path src/main/resources/db/ และทุกครั้งที่ Run project จะวิ่งมาอ่านไฟล์นี้เพื่อเช็คว่ามีอะไร Change บ้าง และถ้ามีการสร้างไฟล์ .xml ใหม่ต้องมาเพิ่มในนี้ด้วย

<databaseChangeLog
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">

    <!-- Table -->
    <include file="/db/changelogs/table/xxx01.xml"/>
    <include file="/db/changelogs/table/xxx02.xml"/>

    <!-- View -->
    <include file="/db/changelogs/view/v_xxx01.xml"/>
    <include file="/db/changelogs/view/v_xxx02.xml"/>

</databaseChangeLog>
. . .

การทำงานของ Liquibase

ถ้า Run project ครั้งแรกหลังจากเพิ่ม Maven dependency และ Config เรียบร้อยแล้ว Spring boot จะสร้างตารางที่ชื่อว่า

  • databasechangelog - เก็บ Logs ของการ Run script ทุกครั้ง โดยใช้ id เป็น Primary key
ColumnData typeDescription
IDVARCHAR(255)id ของ changeSet
AUTHORVARCHAR(255)author ของ changeSet
FILENAMEVARCHAR(255)Path file ของ changelog
DATEEXECUTEDDATETIMEDate/time ที่ changeSet นั้นๆ Execute
ORDEREXECUTEDINTลำดับของการ Executed
EXECTYPEVARCHAR(10)EXECUTED, FAILED, SKIPPED, RERAN และ MARK_RAN
DESCRIPTIONVARCHAR(255)คำอธิบายสั้นๆ ว่า changeSet นี้ทำอะไร

ตัวอย่างข้อมูลในตาราง

Database Changelog

หลักๆแล้ว Column ทีเราสนใจจะมีประมาณนี้ แต่ Column ทั้งหมดมีมากกว่านี้

  • databasechangeloglock - ถ้าเกิดมี Developer 2 คน Run script นั้นๆ ไปที่ Database เดียวกันพร้อมกัน Liquibase จะถูก Lock เพื่อป้องกัน Transaction conflict สามารถ Unlock ได้ด้วยการ Run script UPDATE DATABASECHANGELOGLOCK SET LOCKED=false
ColumnData typeDescription
IDINTปัจจุบันมีแค่ id = 1
LOCKEDBOOLEANtrue = Locked , false = Unlocked
LOCKGRANTEDDATETIMEวันที่และเวลาที่ Unlocked
LOCKEDBYVARCHAR(255)ใครเป็นคนทำให้ Locked

Spring boot จะรู้ได้อย่างไร ?

Spring boot จะรู้ว่ามี Script เพิ่มมาใหม่ก็ต่อเมื่อ id นั้นไม่มีใน Database โดยจะมาเช็คไฟล์ .xml และ Compare กับ Database ทุกครั้ง

. . .

Syntax

Tags และ Attibutes ทั้งหมดที่มีให้ใช้ โดยจะให้เห็นโครงสร้างทั้งหมดด้วยว่า Tag ไหนต้องอยู่ตรงไหน แต่ล่ะ Tag มี Attibute อะไรบ้าง

ซึ่งผมจะไม่อธิบายและยกตัวอย่างทั้งหมด จะเลือกเฉพาะอันที่ผมเคยใช้เท่านั้น สามารถอ่านเพิ่มเติมได้ที่นี่ Liquibase Documents

    <changeSet id="action-entitie-datetime" author="author_name" >

        <!-- Sub Tags -->
        <comment>
            <!-- Description of this changeSet  -->
        </comment>

        <preConditions >
                <changeSetExecuted id="action-entitie-datetime" author="author_name" changeLogFile="changelog.xml"/>
                <columnExists tableName="my_table" columnName="my_column"/>
                <tableExists tableName="my_table"/>
                <viewExists viewName="my_view"/>
                <foreignKeyConstraintExists foreignKeyName="my_foreignKey"/>
                <indexExists indexName="my_index" tableName="my_table" columnNames="my_table"/>
                <sequenceExists sequenceName="my_sequence" />
                <primaryKeyExists primaryKeyName="my_primaryKey" tableName="my_table"/>
                <sqlCheck expectedResult="my_expectedResult">
                    SELECT COUNT(1) FROM my_table WHERE user = 'liquibase'
                </sqlCheck>
        </preConditions>

        <validCheckSum>
            <!-- Get data from column 'md5sum'  -->
        </validCheckSum>

        <rollback>
            <!-- Sql Script or Change Type -->
        </rollback>

        <!-- Any Refactoring Tags -->
        <!-- Entities -->
            <!-- Table -->
                <createTable></createTable>
                <dropTable></dropTable>
                <renameTable></renameTable>
            <!-- Column -->
                <addColumn></addColumn>
                <dropColumn></dropColumn>
                <renameColumn></renameColumn>
                <modifyDataType></modifyDataType>
                <setColumnRemarks></setColumnRemarks>
                <addAutoIncrement></addAutoIncrement>
            <!-- Index -->
                <createIndex></createIndex>
                <dropIndex></dropIndex>
            <!-- View -->
                <createView></createView>
                <dropView></dropView>
                <renameView></renameView>
            <!-- Procedure -->
                <createProcedure></createProcedure>
                <dropProcedure></dropProcedure>
            <!-- Sequence -->
                <createSequence></createSequence>
                <dropSequence></dropSequence>
                <renameSequence></renameSequence>
                <alterSequence ></alterSequence>

        <!-- Constraints -->
            <!-- Default value -->
                <addDefaultValue></addDefaultValue>
                <dropDefaultValue></dropDefaultValue>
            <!-- Foreign key -->
                <addForeignKeyConstraint></addForeignKeyConstraint>
                <dropForeignKeyConstraint></dropForeignKeyConstraint>
                <dropAllForeignKeyConstraints></dropAllForeignKeyConstraints>
            <!-- Not null -->
                <addNotNullConstraint></addNotNullConstraint>
                <dropNotNullConstraint></dropNotNullConstraint>
    </changeSet>
. . .

changeSet

เพื่อแบ่งกลุ่มการทำงานของ Script

  • id - แต่ล่ะ changeSet ห้ามมี id ที่ซ้ำกัน เพราะจะใช้เป็น Primary key ใน Database ถ้าซ้ำจะเกิด Error และควรตั้งชื่อ id ให้สื่อความหมายด้วย เช่น สร้างตาราง จะตั้งเป็น create-table-my_table-MMDDYYYhhmm เพื่อจะได้ไม่ซ้ำกัน
  • author - ชื่อคนที่เขียน Script นี้ เพื่อ Track กลับมาได้ ถ้าเกิดมีปัญหา

Sub tags

comment

ระบุรายละเอียดของ changeSet ว่าทำอะไร ทำไมต้องทำแบบนี้ เป็นต้น และสามารถเอารายละเอียดไปทำเป็นเอกสารประกอบได้ถ้าต้องการ

. . .

preConditions

เป็นการเช็คเงื่อนไขต่างๆตามที่เรากำหนด ถ้าไม่ผ่าน จะให้ทำอะไรต่อ ซึ่งมีให้เลือกใช้หลายแบบ อ่านเพิ่มเติม

  • onFail - เป็นการบอกว่าจะให้ทำอะไรต่อ ถ้าการเช็คเงื่อนไขเกิด Fail ขึ้นมา ซึ่งมีทั้งหมด 4 ทางเลือก

    • HALT - สามารถเอาไว้นอก changeSet ได้ โดยเอาไว้แรกสุดของ changelog ถ้าเงื่อนไขไม่ผ่าน จะไม่ทำ changeSet ที่เหลือทั้งหมดใน changelog นั้น เช่น
    <preConditions onFail="HALT">
        ...
    </preConditions>
    
    <changeSet id="1" author="liquibase">
        ...
    </changeSet>
    
    <changeSet id="2" author="liquibase">
        ...
    </changeSet>
    

    ถ้าเงื่อนไขใน preConditions ไม่ผ่าน จะไม่ทำ changeSet ที่ id = 1 และ 2 ต่อ

    • CONTINUE - เป็นการข้าม changeSet นั้นๆ และจะกลับมา execute อีกครั้ง เมื่อ changeSet มีการเปลี่ยนแปลง
    • MARK_RAN - เป็นการข้าม changeSet นั้นๆ แต่จะบันทึกลง databasechangelog ว่าได้ทำการ execute ไปแล้ว
    • WARN - ส่งคำเตือน และดำเนินการ changeSet ต่อและบันทึก changelog ตามปกติ สามารถเอาไว้นอก changeSet ได้ เหมือนกับ HALT แต่จะไม่หยุดการทำงาน

ส่วนตัวแล้วจะผมใช้แค่ MARK_RAN

preConditions มีอะไรให้ใช้บ้าง ?

  • changeSetExecuted

เป็นการตรวจสอบว่า changeSet ที่ระบุได้ทำการ execute ไปแล้วหรือยัง

<!-- ตรวจสอบว่าต้องยังไม่ execute ถ้าพบว่า execute ไปแล้ว จะ Fail -->
<preConditions onFail...>
    <changeSetExecuted id="action-entitie-datetime" author="author_name" changeLogFile="changelog.xml" />
</preConditions>
  • columnExists

เป็นการตรวจสอบ Column ตามที่ระบุ โดยต้องบอกด้วยว่าที่ Table ไหน ถ้าเช็คว่ามี ส่วนมากจะใช้กับ <dropColumn/> หรือการ Change ต่างๆ แต่ถ้าตรวจสอบว่าไม่มี จะใช้กับ <addColumn/> เพราะจะเพิ่ม Column ได้ต้องไม่มี Column ตามชื่อที่ระบุ

<!-- ตรวจสอบว่าต้องมี ถ้าพบว่าไม่มี จะ Fail -->
<preConditions onFail...>
    <not>
        <columnExists tableName="my_table" columnName="my_column" />
    </not>
</preConditions>

<!-- ตรวจสอบว่าต้องไม่มี ถ้าพบว่ามี จะ Fail-->
<preConditions onFail...>
    <columnExists tableName="my_table" columnName="my_column" />
</preConditions>
  • tableExists

เป็นการตรวจสอบ Table ตามที่ระบุ ถ้าเช็คว่ามี ส่วนมากจะใช้กับ <dropTable/> หรือการ Change ต่างๆ แต่ถ้าตรวจสอบว่าไม่มี จะใช้กับ <createTable/>

<!-- ตรวจสอบว่าต้องมี ถ้าพบว่าไม่มี จะ Fail -->
<preConditions onFail...>
    <not>
        <tableExists tableName="my_new_table"/>
    </not>
</preConditions>

<!-- ตรวจสอบว่าต้องไม่มี ถ้าพบว่ามี จะ Fail-->
<preConditions onFail...>
    <tableExists tableName="my_new_table"/>
</preConditions>

ผมขอข้าม Change type ที่เป็นกลุ่มของ Exists ไปนะครับ เนื่องจาก Concept จะเหมือนกันหมด ตามที่ได้ยกตัวอย่างของ Column และ Table

  • sqlCheck

เป็นการตรวจสอบ Return value ที่มาจากการเขียน Sql script ซึ่งต้องเป็นการ Return เพียง 1 row หรือ 1 value เท่านั้น

<preConditions onFail...>
    <sqlCheck expectedResult="1">
        SELECT COUNT(1) FROM my_table WHERE user = 'liquibase'
    </sqlCheck>
</preConditions>

. . .

validCheckSum

Tag นี้ เพื่อ Execute ซ้ำอีกครั้ง โดยที่ไม่ Error เพราะปกติแล้ว เราจะไม่สามารถกลับไปแก้ไข changeSet ที่เคย Execute ไปแล้วได้ จะมี Error ต้องสร้าง changeSet เป็น id ใหม่เท่านั้น แต่เราไม่ได้ต้องการสร้าง changeSet ใหม่ จะทำการแก้ไขที่ changeSet เดิม และเพิ่ม <validCheckSum/> โดยเอาค่า Sum ใหม่จาก Error

Validation Failed:
     1 change sets check sum
          com/example/changelog.xml::1::author_name was:
    8:1b99d310bf88ee47d9d7daf1f33f2275 but is now: 8:af6710859b7acfa6538fed7079fe9525
<changeSet id="action-entitie-datetime" author="author_name">
      <validCheckSum>8:af6710859b7acfa6538fed7079fe9525</validCheckSum>
      (new logic here)
</changeSet>
. . .

rollback

สามารถระบุได้ทั้ง SQL และ Change Type หรือ อ้างอิง changeSet อื่นๆได้ ถ้าใน changeSet มีการเรียกใช้ rollback เมื่อเกิด Error จะยกเลิกการทำงานภายใน changeSet นั้นๆทั้งหมด และคืนค่าทุกอย่างกลับไปเป็นแบบเดิม มีทั้งหมด 4 แบบ

  • Change Type
<changeSet id="action-entitie-datetime" author="author_name">
	<createTable tableName="my_table">
	<rollback>
		<dropTable tableName="my_table"/>
	</rollback>
</changeSet>

ถ้า createTable สำเร็จ แต่ dropTable ไม่สำเร็จ จะคืนค่าทุกอย่าง เท่ากับว่า คำสั่ง createTable จะไม่มีผล ตารางจะไม่ถูกสร้าง แต่ถ้าไม่มี rollback ครอบไว้ ตารางจะถูกสร้าง และอีกตารางก็จะไม่ถูก drop

  • อ้างอิง changeSet อื่น
<changeSet id="action-entitie-datetime-1" author="author_name-1">
	<dropTable tableName="my_table"/>
	<rollback changeSetId="action-entitie-datetime-2" changeSetAuthor="author_name-2"/>
</changeSet>

ถ้า changeSet ที่เราอ้างอิง เกิดข้อผิดพลาด จะกลับมา rollback ที่ changeSet นี้

  • SQL
<changeSet id="action-entitie-datetime" author="author_name">
	<createTable tableName="my_table"/>
	<rollback >
        <sql>
            drop table my_table;
        </sql>
    </rollback>
</changeSet>

สามารถเขียนเป็น SQL ได้ ถ้าแบบ Change Type ไม่สามารถทำได้

ข้อควรระวัง ถ้าเขียนแบบ SQL ต้องอิงตาม Syntax ของ Database แต่ล่ะประเภท


Entities

หลักๆที่ผมใช้มี แบ่งได้ตามนี้ครับ

Table

  • createTable

เงื่อนไข preConditions คือ ต้องไม่มีตาราง employee

<changeSet id="create_table_employee_DDMMYYYY" author="author_name">
    <preConditions onFail="MARK_RAN">
        <not>
            <tableExists tableName="employee"/>
        </not>
    </preConditions>
	<createTable tableName="employee">
        <column name="id" type="int">
           <constraints primaryKey="true"/>
        </column>
        <column name="employee_no" type="varchar(10)" remarks="รหัสพนักงาน">
           <constraints nullable="false"/>
        </column>
    </createTable>
</changeSet>
  • createTable (SQL)
<changeSet id="action-entitie-datetime" author="author_name">
	<preConditions onFail="MARK_RAN">
        <not>
            <tableExists tableName="member"/>
        </not>
    </preConditions>
    <sql>
        CREATE TABLE public."member" (
                id int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY,
                created_by varchar(255) NULL,
                created_date timestamp NULL,
                updated_by varchar(255) NULL,
                updated_date timestamp NULL,
                "name" varchar(255) NULL,
                status varchar(255) NULL,
                CONSTRAINT member_pkey PRIMARY KEY (id)
        );
        comment on column public."member".name is 'ชื่อสมาชิก';
    </sql>
</changeSet>
  • dropTable

เงื่อนไข preConditions คือ ต้องมีตาราง users

<changeSet id="drop_table_users_DDMMYYYY" author="author_name">
    <preConditions onFail="MARK_RAN">
        <tableExists tableName="users" />
    </preConditions>

    <dropTable tableName="users"/>
</changeSet>
. . .

Column

  • addColumn

เงื่อนไข preConditions คือ ต้องมีตาราง member และ ต้องไม่มี column active

<changeSet id="add_column_active_table_member_DDMMYYYY" author="author_name">
    <preConditions onFail="MARK_RAN">
        <tableExists tableName="member" />
        <not>
            <columnExists tableName="member" columnName="active"/>
        </not>
    </preConditions>
    <addColumn tableName="member">
        <column name="active" type="varchar(1)" remarks="Flag Y : ใช้งาน , N : ไม่ใช้งาน" defaultValue="Y"/>
    </addColumn>
</changeSet>
  • dropColumn

เงื่อนไข preConditions คือ ต้องมีตาราง users และ ต้องมี column version

<changeSet id="drop_column_version_table_users_DDMMYYYY" author="author_name">
    <preConditions onFail="MARK_RAN">
        <tableExists tableName="users" />
        <columnExists tableName="users" columnName="version"/>
    </preConditions>
    <dropColumn tableName="users" columnName="version"/>
</changeSet>
  • SQL
<changeSet id="alter_column_phone_number_table_users_DDMMYYYY" author="author_name">
    <preConditions onFail="MARK_RAN">
        <tableExists tableName="users" />
        <columnExists tableName="users" columnName="phone_number"/>
    </preConditions>
    <sql>
        ALTER TABLE users ALTER COLUMN phone_number DROP NOT NULL;
    </sql>
</changeSet>

<changeSet id="alter_column_fine_time_table_groups_DDMMYYYY" author="author_name">
    <preConditions onFail="MARK_RAN">
        <tableExists tableName="groups" />
        <columnExists tableName="groups" columnName="fine_time"/>
    </preConditions>
    <sql>
        ALTER TABLE public."groups" DROP COLUMN fine_time;
        ALTER TABLE public."groups" ADD fine_time int4 NULL;
    </sql>
</changeSet>
. . .

View

ถ้า view มีการแก้ไขอะไรก็ตาม ผมใช้วิธีเปลี่ยน id changeSet ไปเรื่อยๆ v1 , v2 , v3 ... และเปลี่ยน DDMMYYYY ไม่ต้องสร้าง changeSet ใหม่ ต้องใช้คู่กับการ drop view

  • createView

preConditions คือ ต้องมีตารางที่เกี่ยวข้องทั้งหมดที่ใช้ใน view v_name_02

<changeSet id="create_view_v_name_02_v1_DDMMYYYY" author="author_name">
    <preConditions onFail="MARK_RAN">
        <tableExists tableName="groups" />
        <tableExists tableName="member" />
        <viewExists viewName="v_name_01" />
    </preConditions>
    <sqlFile path="/db/sql/v_name_02.sql" encoding="UTF-8" />
</changeSet>

/db/sql/v_name_02.sql

create view v_name_02
as select
   ...
from
    v_name_01 v01
    left join "groups" g on ...
    left join "member" m on ...
  • dropView

ทกุ changeSet การ drop view ผมจะรวมไว้ใน /db/changelogs/view/drop.xml

<changeSet id="drop_view_v_name_02_v1_DDMMYYYY" author="author_name">
    <preConditions onFail="MARK_RAN">
        <viewExists viewName="v_name_02"/>
    </preConditions>
    <sql>
        DROP VIEW v_name_02;
    </sql>
</changeSet>

และลำดับการอ่าน .xml ใน db/db-changelog-master.xml ผมทำแบบนี้ drop ก่อนเสมอแล้วค่อย create

<include file="/db/changelogs/view/drop.xml"/>

<include file="/db/changelogs/view/v_name_02.xml"/>

เวลามีการอัพเดต view เราแค่มาเปลี่ยน version และวันที่ใน id changeSet ทำให้ Spring Boot เห็นว่าเป็น id ใหม่ และมา Run .xml นั้นๆ

. . .

Sequence

  • dropSequence
<changeSet id="drop_table_users_DDMMYYYY" author="author_name">
    <preConditions onFail="MARK_RAN">
        <sequenceExists sequenceName="seq_users_id" />
    </preConditions>

    <dropSequence sequenceName="seq_users_id"/>
</changeSet>
  • SQL
<changeSet id="create_sequence_hibernate_sequence_DMMYYYY" author="author_name">
    <preConditions onFail="MARK_RAN">
        <not>
            <sequenceExists sequenceName="hibernate_sequence"/>
        </not>
    </preConditions>
    <sql>
        CREATE SEQUENCE hibernate_sequence;
    </sql>
</changeSet>
. . .

Data

สำหรับ master data ที่จำเป็นต้องใช้ในการ set up project เช่น parameter ต่างๆ

  • Insert

อันนี้จะเป็น member ตั้งต้น ที่ต้องมี อาจจะเป็น admin ของระบบ เป็นต้น และใน preConditions ผมจะเขียน sql เช็คด้วยว่าต้องไม่มี member นี้

<changeSet author="author_name" id="insert_member_DDMMYYYY">
    <preConditions onFail="MARK_RAN">
        <and>
            <tableExists tableName="member" />
        </and>
        <and>
            <sqlCheck expectedResult="0">
                select count(id) from member where name = 'admin';
            </sqlCheck>
        </and>
    </preConditions>
    <sql>
        INSERT INTO public."member"
        (id, created_by, created_date, updated_by, updated_date, name)
        VALUES(default,'system',now(),'system',null,'admin');
    </sql>
</changeSet>

ซึ่งการใช้งานส่วนใหญ่ของผมก็จะประมาณนี้ จะมากน้อยขึ้นอยู่กับความซับซ้อน และการออกแบบ Database ด้วย ยังมี Function อีกมากมายที่ผมยังไม่ได้ใช้เหมือนกัน ที่ผมได้นำเรื่อง Liquibase มาเขียน Blog เพราะผมได้นำไปช้ใน Project ที่เป็นงานจริงๆ และมันช่วยแก้ปัญหา Human error ได้มากเลยทีเดียว ทุกครั้งที่ Deploy ผมไม่ต้องมานั่งกังวลว่าจะลืม Run script อะไรไปหรือเปล่า มีอะไรตกหล่นไปหรือไม่(ถ้าตอน dev ไม่ได้มีใครไปทำอะไร manual ที่ db อ่ะนะ) ผมหวังว่า Blog นี้จะเป็นประโยชน์ไม่มากก็น้อยแก่ผู้อ่านทุกท่านครับ 😅