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

## Installation

1. ลง Liquibase ใน pom.xml
   

    <dependencies>
    	<dependency>
    		<groupId>org.liquibase</groupId>
    		<artifactId>liquibase-core</artifactId>
    		<version>3.6.2</version>
    	</dependency>
    </dependencies>
    

2. เพิ่ม 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
```

2. สร้าง 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 ของมันเองด้วย

```xml
<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 ให้อยู่แล้ว

```java
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](https://www.baeldung.com/liquibase-refactor-schema-of-java-app)
- วิธีเพิ่ม unique constraint ให้ table [https://docs.liquibase.com/change-types/community/add-unique-constraint.html](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](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](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/](https://reflectoring.io/database-migration-spring-boot-liquibase/)
- อันนี้ช่วยหาคำตอบให้ตอนงงๆ [liquibase.properties](http://liquibase.properties) ว่าตกลงใช้ไม่ใช้ [https://watson.earth/2018/09/21/combining-spring-boot-and-liquibase/](https://watson.earth/2018/09/21/combining-spring-boot-and-liquibase/)
- อันนี้ดูค้างไว้เรื่อง Rollback, pre-condition ของ changeset ว่าใช้ยังไง [https://docs.liquibase.com/concepts/advanced/preconditions.html](https://docs.liquibase.com/concepts/advanced/preconditions.html)
- อันนี้ดีมาก เป็น Best Practice ของ Liquibase เองเลย หลายๆ อย่างได้มาจากอันนี้แหละ [https://www.liquibase.org/get-started/best-practices](https://www.liquibase.org/get-started/best-practices)
- อันนี้พูดถึง liquibase-maven-plugin โดยเฉพาะ [https://docs.liquibase.com/tools-integrations/maven/home.html](https://docs.liquibase.com/tools-integrations/maven/home.html)
