Database migration sử dụng Liquibase

Bài viết được sự cho phép của tác giả Nguyễn Hữu Khanh

Database migration hay chúng ta còn có thể gọi là version control cho database, là một công việc quản lý thông tin, cấu trúc database theo kiểu versioning. Lấy ví dụ như bạn đang phát triển một ứng dụng quản lý sinh viên có sử dụng database, release đầu tiên của ứng dụng này các bạn cần 2 table để quản lý thông tin là student và clazz, lần release thứ 2 thì chúng ta cần chỉnh sửa thông tin table student hoặc clazz, hoặc có thể là thêm mới table subject để quản lý môn học, … Cho mỗi lần release chúng ta sẽ có một version liên quan đến database structure cho ứng dụng của mình. Để hiện thực nhu cầu này, chúng ta có nhiều cách khác nhau trong Java như sử dụng Flyway hoặc Liquibase, … Trong bài viết này, mình sẽ giới thiệu với các bạn về Liquibase để hiện thực database migration các bạn nhé!

Xem thêm Tuyển dụng database hấp dẫn trên TopDev

Đầu tiên, mình sẽ tạo mới một Maven project để làm ví dụ:

Database migration sử dụng Liquibase

Mình sẽ sử dụng Java 11 cho project này:

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>

Để làm việc với Liquibase, các bạn cần khai báo dependency của nó:

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

Mình cũng sẽ sử dụng PostgreSQL database để làm ví dụ cho bài viết này:

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.20</version>
</dependency>

Khi làm việc với Liquibase, các bạn cần nắm 3 khái niệm cơ bản của nó là changelog, changeset và changetype.

Một changelog có thể sẽ chứa nhiều changeset và một changeset có thể chứa nhiều changetype. Nói nôm na cho các bạn hiểu thì changelog là một tập tin này định nghĩa tất cả các version của database structure theo cách của Liquibase để nó có thể execute việc thay đổi database theo version cho chúng ta được. Các bạn có thể định nghĩa tập tin changelog này theo nhiều định dạng khác nhau bao gồm SQL, XML, JSON, YAML, …

Nếu các bạn định nghĩa bằng XML thì nội dung của tập tin changelog sẽ có nội dung cơ bản như sau:

<?xml version="1.0" encoding="UTF-8"?>
<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-4.3.xsd">

<!-- Define changeset -->

</databaseChangeLog>

Bên trong tag <databaseChangeLog> đại diện cho changelog, chúng ta sẽ định nghĩa các changeset. Các bạn có thể hiểu mỗi changeset là một version của database structure của ứng dụng. Có 2 thuộc tính bắt buộc cho mỗi changeset mà các bạn phải khai báo là id và author.

<?xml version="1.0" encoding="UTF-8"?>
<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-4.3.xsd">

<!-- Define changeset -->
<changeSet id="1" author="khanh">
<comment>A sample change set 1</comment>
</changeSet>

<changeSet id="2" author="khanh">
<comment>A sample change set 2</comment>
</changeSet>
</databaseChangeLog>

Mỗi changeset sẽ hỗ trợ cho các bạn rất nhiều changetype. Nói nôm na changetype là đại diện cho các câu lệnh SQL để các bạn thực hiện việc thêm, mới, xoá, sửa database structure. Cho changeLog với XML format, các bạn có thể thấy các changetype như sau:

Database migration sử dụng Liquibase

Ví dụ bây giờ mình cần tạo mới table name student với thông tin về tên và tuổi của sinh viên thì mình sẽ định nghĩa changeset với changetype như sau:

<?xml version="1.0" encoding="UTF-8"?>
<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-4.3.xsd">

<changeSet id="1" author="khanh">
<createTable tableName="student">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false" />
</column>
<column name="name" type="VARCHAR(200)" />
<column name="age" type="BIGINT" />
</createTable>
</changeSet>
</databaseChangeLog>

Bây giờ, mình sẽ viết code để xem Liquibase hoạt động như thế nào các bạn nhé!

Mình sẽ lưu nội dung changelog ở trên vào tập tin db-changelog.xml trong thư mục src/main/resources của project:

Database migration sử dụng Liquibase

Mình sẽ tạo mới main class để làm ví dụ:

package com.huongdanjava.liquibase;

public class Application {

public static void main(String[] args) {

}

}

Để làm việc với Liquibase, các bạn cần sử dụng đối tượng của class Liquibase. Chúng ta sẽ cần truyền cho đối tượng này connection tới database và đường dẫn tới tập tin cấu hình của changelog. Có 3 constructor trong class Liquibase cho phép chúng ta truyền các thông tin này.

Mình sử dụng một trong 3 constructor này như sau:

package com.huongdanjava.liquibase;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import liquibase.Liquibase;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.LiquibaseException;
import liquibase.resource.ClassLoaderResourceAccessor;

public class Application {

public static void main(String[] args) throws SQLException, LiquibaseException {
Connection c = DriverManager.getConnection("jdbc:postgresql://localhost:5432/liquibase_example",
"postgres", "123456");

Database database = DatabaseFactory.getInstance()
.findCorrectDatabaseImplementation(new JdbcConnection(c));

Liquibase liquibase = new Liquibase("classpath:db-changelog.xml", new ClassLoaderResourceAccessor(), database);
}

}

Tham số thứ 2 trong constructor của Liquibase trong ví dụ trên chỉ định cách mà Liquibase sẽ đọc tập tin cấu hình changelog. Nó implement interface ResourceAccessor của Liquibase đó các bạn!

Các bạn sử dụng cách nào cũng được tùy theo project của mình nhé.

Sau khi đã có đối tượng Liquibase thì các bạn có thể gọi method update() với một tham số mang ý nghĩa Context mà chúng ta đang chạy database migration. Các bạn có thể để empty hoặc null cũng được nếu muốn. Mình sẽ để empty:

package com.huongdanjava.liquibase;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import liquibase.Liquibase;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.LiquibaseException;
import liquibase.resource.ClassLoaderResourceAccessor;

public class Application {

public static void main(String[] args) throws SQLException, LiquibaseException {
Connection c = DriverManager.getConnection("jdbc:postgresql://localhost:5432/liquibase_example",
"postgres", "123456");

Database database = DatabaseFactory.getInstance()
.findCorrectDatabaseImplementation(new JdbcConnection(c));

Liquibase liquibase = new Liquibase("classpath:db-changelog.xml", new ClassLoaderResourceAccessor(), database);

liquibase.update("");
}

}

OK, giờ thì chạy ứng dụng thôi các bạn.

Để kiểm tra kết quả, các bạn hãy vào database của mình. Các bạn sẽ thấy Liquibase tạo ra 3 bảng trong ví dụ trên của mình:

Database migration sử dụng Liquibase

Ngoài bảng student với cấu trúc database mà chúng ta đã định nghĩa trong changeset:

Database migration sử dụng Liquibase

như các bạn thấy, chúng ta còn có thêm 2 bảng là databasechangelog và databasechangeloglock.

Mục đích của bảng databasechangelog là keep track lại tất cả các changeset mà Liquibase đã chạy. Nếu các bạn query table này, các bạn sẽ thấy kết quả như sau:

Database migration sử dụng Liquibase

Thứ tự execute cho mỗi changeset sẽ phụ thuộc vào việc các bạn định nghĩa changeset đó trước hay sau trong tập tin db-changelog.xml các bạn nhé! Id của mỗi changeset cùng với author và filename chỉ để cho Liquibase biết là nó đã execute changeset đó chưa dựa vào bảng databasechangelog này.

  26 công cụ và kỹ thuật trong Big Data có thể bạn chưa biết
  Cấu hình đồng bộ hai database mysql server MySQL Replication

Bảng databasechangeloglock chỉ để cho Liquibase make sure là chỉ có một tiến trình thực hiện việc database migration được thực hiện tại cùng một thời điểm thôi các bạn nhé!

Bây giờ, nếu mình thêm một changeset mới để thêm column address trong bảng student:

<?xml version="1.0" encoding="UTF-8"?>
<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-4.0.xsd">

<changeSet id="1" author="khanh">
<createTable tableName="student">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false" />
</column>
<column name="name" type="VARCHAR(200)" />
<column name="age" type="BIGINT" />
</createTable>
</changeSet>
<changeSet id="2" author="khanh">
<addColumn tableName="student">
<column name="address" type="VARCHAR(200)" />
</addColumn>
</changeSet>
</databaseChangeLog>

thì các bạn sẽ thấy kết quả như sau:

Database migration sử dụng Liquibase