Liquibase管理数据库版本

words: 2k    views:    time: 9min

Liquibase是一个用于用于跟踪和管理应用数据库变化的开源工具,通过changelog日志的形式来记录数据库的变更,然后执行日志文件中的修改,将数据库更新或回滚到一致的状态。它的目标是提供一种与数据库类型无关的解决方案,主要优点包括:

  • 支持几乎所有主流的数据库,包括Oracle、Sql Server、DB2、MySql、Sybase、PostgreSQL等,这样在数据库的部署和升级环节可以支持多数据库;
  • 日志文件支持多种格式,如XML、YAML、JSON、SQL等;
  • 支持回滚功能,可以按时间、数量、标签回滚变化;

官网文档:https://docs.liquibase.com/home.html

思路

在使用Liquibase之前,我们也想过一些办法来进行数据库版本管理。思路是在库中约定一张版本表,用来记录每次数据库的变化版本,本地也会给每次数据库升级的sql文件定义一个版本进行维护,然后每次在升级应用之前,先人工比较升级下数据库版本。这样可以解决数据库与应用版本之间的对应关系,但是要依赖人工检查,而且将数据库与应用的版本分开管理也容易出现不一致问题。

Liquibase解决了上面两个问题,它将人工检查数据库版本的工作进行了自动化(只要数据库实例存在就行),并且将数据库的变化内容放在应用代码中进行维护。当然它还进一步也做了很多工作,比如为了避免多个应用实例升级时对同一个数据库的操作,它加了一张表来进行同步控制,以及在有的场景中可能要根据参数判断对不同环境的数据库执行不同的操作等等。

示例

这里以springboot 2.7.0为例来使用Liquibase

  • 依赖
pom.xml
1
2
3
4
5
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>4.9.1</version>
</dependency>
  • Liquibase配置

虽然Liquibase提供了xml/yaml格式来描述数据库变化,方便支持不同数据库,但我们还是倾向于使用直接的sql文件来描述。

application.yml
1
2
3
4
5
6
7
8
9
10
11
12
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: org.postgresql.Driver
url: jdbc:postgresql://127.0.0.1:5432/test
username: postgres
password: postgres
liquibase:
enabled: true
change-log: sql/changelog.yml
parameters:
cluster: 10
  • changelog

一个changeSet可以包含多个change,对应数据库的一条日志记录,所以它们共用tag、labels、comment等属性。change的类型可以是sqlFile、createProduce或其他更多类型,具体可以查看官方文档。另外,还可以对change配置条件执行,这个在文档中没有找到,不过看它代码找到了,它里面维护了一个map,并且在触发时会进行检查。

sql/changelog.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
databaseChangeLog:
- changeSet:
id: init
labels: init
author: shanhm1991
comment: 初始化
changes:
- sqlFile:
splitStatements: true
stripComments: false
path: sql/init.ddl.sql
- sqlFile:
splitStatements: true
stripComments: false
path: sql/init.dml.sql
- tagDatabase:
tag: 1.0.0
- changeSet:
id: update.20220421
labels: update.20220421
author: shanhm1991
comment: 更新xxx
preConditions: ## 对应application.yml中的参数配置cluster,只要值为10的时候才执行
- onFail: MARK_RAN
- changeLogPropertyDefined:
property: cluster
value: 10
changes:
- sqlFile:
splitStatements: false
stripComments: false
path: sql/update.20220421.sql
- tagDatabase:
tag: 1.0.1
  • sql
sql/init.ddl.sql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
drop table if exists sys_project;
create table sys_project(
id bigserial primary key,
project_code character varying(64),
project_name character varying(255),
archive_version character varying(64),
archive_time timestamp,
remark character varying(512),
create_user int8,
create_time timestamp,
update_user int8,
update_time timestamp
);
create unique index sys_project_unique on sys_project(project_code);
sql/init.dml.sql
1
2
insert into sys_project(project_code,project_name,archive_version,archive_time,remark)
values ('xxx-test','xxx-test','1.0.0','2022-01-01','test');
sql/update.20220421.sql
1
2
3
4
5
6
7
8
9
10
11
12
13
create or replace function dataFill() returns boolean AS
$BODY$
declare i integer;
begin
FOR i IN 1..100 LOOP
insert into sys_project(project_code,project_name,archive_version,archive_time,remark)
values ('xxx-test' || i,'xxx-test','1.0.0','2022-01-01','test');
end loop;
return true;
end;
$BODY$
LANGUAGE plpgsql;
select * from dataFill() as tab;
  • 结果

除了context字段没有用到,版本记录表字段与上面的配置基本对应

  • 日志

从日志我们可以看到Liquibase对锁的获取和释放操作,并且是以Changeset作为单位来执行并记录tag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2023-12-19 11:40:30.508  INFO 38857 --- [main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
2023-12-19 11:40:31.011 INFO 38857 --- [main] liquibase.database [ Set default schema name to public
2023-12-19 11:40:31.079 INFO 38857 --- [main] liquibase.lockservice [ Successfully acquired change log lock
2023-12-19 11:40:31.145 INFO 38857 --- [main] liquibase.changelog [ Creating database history table with name: public.databasechangelog
2023-12-19 11:40:31.159 INFO 38857 --- [main] liquibase.changelog [ Reading from public.databasechangelog
Running Changeset: sql/changelog.yml::init::shanhm1991
2023-12-19 11:40:31.206 INFO 38857 --- [main] liquibase.changelog [ SQL in file sql/init.ddl.sql executed
2023-12-19 11:40:31.210 INFO 38857 --- [main] liquibase.changelog [ SQL in file sql/init.dml.sql executed
2023-12-19 11:40:31.211 INFO 38857 --- [main] liquibase.changelog [ Tag '1.0.0' applied to database
2023-12-19 11:40:31.211 INFO 38857 --- [main] liquibase.changelog [ ChangeSet sql/changelog.yml::init::shanhm1991 ran successfully in 30ms
Running Changeset: sql/changelog.yml::update.20220421::shanhm1991
2023-12-19 11:40:31.227 INFO 38857 --- [main] liquibase.changelog [ SQL in file sql/update.20220421.sql executed
2023-12-19 11:40:31.227 INFO 38857 --- [main] liquibase.changelog [ Tag '1.0.1' applied to database
2023-12-19 11:40:31.228 INFO 38857 --- [main] liquibase.changelog [ ChangeSet sql/changelog.yml::update.20220421::shanhm1991 ran successfully in 12ms
2023-12-19 11:40:31.231 INFO 38857 --- [main] liquibase.lockservice [ Successfully released change log lock

注意问题

以下只是我们在实践过程遇到记录的一些问题,更多问题可以查看官方文档

  • 已经执行过的sql文件不要修改,否则会启动失败。Liquibase会校验文件的md5,原则应该只新增不修改;
  • 在容器初始化完成前不要执行sql操作,比如@PostConstruct中,建议将操作放到ApplicationRunner中。因为Liquibase完成之前可能会数据库版本不对而导致错误,而Liquibase又依赖datasource实例的初始化;
  • 如果希望区分环境来执行不同的sql,可以通过定义参数,以及preConditions来进行控制,具体见上面示例;
  • 如果是存储过程或函数,将splitStatements改为false,默认会以分号”;”作为一条语句结束。也尝试过用createProduce来定义change,但是发现事务并没有执行也没有任何提示,没有过多研究,直接不分行整个作为sqlFile作为一个sql执行可以了。

数据库适配

如果使用的数据库不在Liquibase的支持列表中,可能要进行一些适配修改,下面记录一下自己适配OSCAR神通数据库的实践,具体修改记录可以查看仓库(分支oscar.4.9.1):https://github.com/shanhm1991/liquibase

  • 首先添加OSCARDatabase定义,这个可以复制一下OracleDatabase,然后稍微改改就行
https://github.com/shanhm1991/liquibase/blob/oscar.4.9.1/liquibase-core/src/main/java/liquibase/database/core/OSCARDatabase.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class OSCARDatabase extends AbstractJdbcDatabase {
public static final Pattern PROXY_USER = Pattern.compile(".*(?:thin|oci)\\:(.+)/@.*");

public static final String PRODUCT_NAME = "OSCAR";
private static ResourceBundle coreBundle = getBundle("liquibase/i18n/liquibase-core");
protected final int SHORT_IDENTIFIERS_LENGTH = 30;
protected final int LONG_IDENTIFIERS_LEGNTH = 128;
public static final int ORACLE_12C_MAJOR_VERSION = 12;

private Set<String> reservedWords = new HashSet<>();
private Set<String> userDefinedTypes;
private Map<String, String> savedSessionNlsSettings;

private Boolean canAccessDbaRecycleBin;
private Integer databaseMajorVersion;
private Integer databaseMinorVersion;

/**
* Default constructor for an object that represents the Oracle Database DBMS.
*/
public OSCARDatabase() {
super.unquotedObjectsAreUppercased = true;
//noinspection HardCodedStringLiteral
super.setCurrentDateTimeFunction("SYSTIMESTAMP");
// Setting list of Oracle's native functions
//noinspection HardCodedStringLiteral
dateFunctions.add(new DatabaseFunction("SYSDATE"));
//noinspection HardCodedStringLiteral
dateFunctions.add(new DatabaseFunction("SYSTIMESTAMP"));
//noinspection HardCodedStringLiteral
dateFunctions.add(new DatabaseFunction("CURRENT_TIMESTAMP"));
//noinspection HardCodedStringLiteral
super.sequenceNextValueFunction = "%s.nextval";
//noinspection HardCodedStringLiteral
super.sequenceCurrentValueFunction = "%s.currval";
}

@Override
public int getPriority() {
return 6;
}

// ... ...

}
  • 然后可以手动注册到DatabaseFactory,也许有自动检测机制,但试了下没效果就直接这里硬编码了
https://github.com/shanhm1991/liquibase/blob/oscar.4.9.1/liquibase-core/src/main/java/liquibase/database/DatabaseFactory.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class DatabaseFactory {
private static final Logger LOG = Scope.getCurrentScope().getLog(DatabaseFactory.class);
private static DatabaseFactory instance;
private Map<String, SortedSet<Database>> implementedDatabases = new HashMap<>();
private Map<String, SortedSet<Database>> internalDatabases = new HashMap<>();

private DatabaseFactory() {
try {
for (Database database : Scope.getCurrentScope().getServiceLocator().findInstances(Database.class)) {
register(database);
}
Database oscar = new OSCARDatabase();
register(oscar);
} catch (Exception e) {
throw new RuntimeException(e);
}

}

}
  • 最后一步是检查所有关于Oracle的判断,增加一个OSCAR,这是最麻烦的,因为源码中充斥着大量的硬编码判断,比如
1
if(database instanceof OracleDatabase) 改为 if(database instanceof OracleDatabase || database instanceof OSCARDatabase)


参考: