不需要在db中手动创建或者导入相关的schema、data,项目启动自动创建对应的表,并初始化。实现该过程。
Liquibase数据库版本管理
依赖配置
在paicoding-web模块中,pom.xml 文件中添加
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
然后就是核心配置,首先是application.yml配置文件中,有两个关键参数
spring:
liquibase:
change-log: classpath:liquibase/master.xml
enabled: true # 当实际使用的数据库不支持liquibase,如 mariadb 时,将这个参数设置为false
说明:
- 对于使用的数据库不支持liquibase,如 mariadb 时,将这个参数设置为false
- change-log:对应的是核心的数据库版本变更配置
master.xml文件中的内容如下:
<?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-3.5.xsd">
<include file="liquibase/changelog/000_initial_schema.xml" relativeToChangelogFile="false"/>
</databaseChangeLog>
注意上面这个include,这个就是告诉liqubase,所有的变更记录,都放在
liquibase/changelog/000_initial_schema.xml这个文件中
现在只有一个include标签,但是实际上是可以有多个的,一个好的建议是,项目首次初始化表、初始化数据可以是一个include标签;后续每次大的版本迭代,对应一个新的include。
再看一下000_initial_schema.xml里面的文件内容
<?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-3.5.xsd">
<property name="now" value="now()" dbms="mysql"/>
<property name="autoIncrement" value="true"/>
<changeSet id="00000000000001" author="YiHui">
<sqlFile dbms="mysql" endDelimiter=";" encoding="UTF-8" path="liquibase/data/init_schema_221209.sql"/>
</changeSet>
<changeSet id="00000000000002" author="YiHui">
<sqlFile dbms="mysql" endDelimiter=";" encoding="UTF-8" path="liquibase/data/init_data_221209.sql"/>
</changeSet>
<changeSet id="00000000000003" author="YiHui">
<sqlFile dbms="mysql" endDelimiter=";" encoding="UTF-8" path="liquibase/data/init_data_221210.sql"/>
</changeSet>
<changeSet id="00000000000005" author="YiHui">
<sqlFile dbms="mysql" endDelimiter=";" encoding="UTF-8" path="liquibase/data/init_data_221216.sql"/>
</changeSet>
<!-- 专栏类型,新增免费开始时间、结束时间 -->
<changeSet id="00000000000006" author="YiHui">
<sqlFile dbms="mysql" endDelimiter=";" encoding="UTF-8" path="liquibase/data/update_schema_221223.sql"/>
</changeSet>
<!-- user_info表添加ip字段,用于记录访问用户所在地 -->
<changeSet id="00000000000007" author="YiHui">
<sqlFile dbms="mysql" endDelimiter=";" encoding="UTF-8" path="liquibase/data/update_schema_221229.sql"/>
</changeSet>
<!-- 配置表新增 extra 字段 -->
<changeSet id="00000000000008" author="YiHui">
<sqlFile dbms="mysql" endDelimiter=";" encoding="UTF-8" path="liquibase/data/update_schema_230103.sql"/>
</changeSet>
<!-- 技术派介绍文章 -->
<changeSet id="00000000000009" author="YiHui">
<sqlFile dbms="mysql" endDelimiter=";" encoding="UTF-8" path="liquibase/data/init_data_230103.sql"/>
</changeSet>
<!-- 重新更新标签 -->
<changeSet id="00000000000012" author="LouZai">
<sqlFile dbms="mysql" endDelimiter=";" encoding="UTF-8" path="liquibase/data/init_data_230105.sql"/>
</changeSet>
<!-- 添加审核中 -->
<changeSet id="00000000000013" author="LouZai">
<sqlFile dbms="mysql" endDelimiter=";" encoding="UTF-8" path="liquibase/data/init_data_230130.sql"/>
</changeSet>
<!-- 添加用户角色 -->
<changeSet id="00000000000014" author="YiHui">
<sqlFile dbms="mysql" endDelimiter=";" encoding="UTF-8" path="liquibase/data/update_schema_230131.sql"/>
</changeSet>
</databaseChangeLog>
说明:
- changeSet标签,id必须唯一,不能出现冲突
- sqlFile里面的path,对应的可以是标准的sql文件,也可以是xml格式的数据库表定义、数据库操作文件
- 一旦写上去,changeSet的顺徐不要调整
比如库表创建的sql
项目演示
在技术派的项目中,还做了一个事情,就是初始化库,在下面的DataSourceInitializer中有介绍;如果是一个新的项目,接入Liquibase之后,数据库,请注意还是需要自己来创建的
项目启动之后,一切正常的话,直接连接上数据库可以看到库表创建成功,数据也初始化完成,当然也可以是直接观察控制台的输出。
下面红框中的ChangeSet xxx run successfully in 401ms 就表示对应的sql执行成功了
注意事项
非常重要的一个点是,上面的每个ChangeSet只会执行一次,因此当执行完毕之后发现不对,要回滚怎么办,或者需要修改怎么办?需要Liquibase提供的回滚机制。简单说明。
当Change执行完毕之后,对应的sql文件/xml文件(即path定义的文件)不允许在修改,因为db中会记录这个文件的md5,当修改这个文件之后,这个MD5也会随之发生改变
有两个方案解决:新增一个changeSte
删除DATABASECHANFELOG表中changeSet id对应的记录,然后重新走一遍
DataSourceInitializer首次初始化方案
我们这里主要借助DataSourceInitializer来实现初始化,其核心有两个配置
- DatabasePopulator:通过addCcripts来指定对应的的sql文件
- DataSourceInitializer#setEnable;判断是否需要执行初始化
我们主要借助DataSourceInitializer来实现Liquibase的表的创建、数据变更等操作;但是在此之前,我们还做了一个库的初始化
库初始化
接下来重点要看的就是needInit方法,我们在这个方法里面,需要判断数据库是否存在,若不存在时,则创建数据库;然后判断表是否存在,以此来决定是否需要执行初始化方法。
入口在实现ForumDataSourceInitializer
/**
* 检测一下数据库中表是否存在,若存在则不初始化;否则基于 schema-all.sql 进行初始化表
*
* @param dataSource
* @return
*/
private boolean needInit(DataSource dataSource) {
if (autoInitDatabase()) {
return true;
}
// 根据是否存在表来判断是否需要执行sql操作
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
List list = jdbcTemplate.queryForList("SELECT table_name FROM information_schema.TABLES where table_name = 'user_info' and table_schema = '" + database + "';");
return CollectionUtils.isEmpty(list);
}
/**
* 数据库不存在时,尝试创建数据库
*/
private boolean autoInitDatabase() {
// 查询失败,可能是数据库不存在,尝试创建数据库之后再次测试
URI url = URI.create(SpringUtil.getConfig("spring.datasource.url").substring(5));
String uname = SpringUtil.getConfig("spring.datasource.username");
String pwd = SpringUtil.getConfig("spring.datasource.password");
try (Connection connection = DriverManager.getConnection("jdbc:mysql://" + url.getHost() + ":" + url.getPort() +
"?useUnicode=true&characterEncoding=UTF-8&useSSL=false", uname, pwd);
Statement statement = connection.createStatement()) {
ResultSet set = statement.executeQuery("select schema_name from information_schema.schemata where schema_name = '" + database + "'");
if (!set.next()) {
// 不存在时,创建数据库
String createDb = "CREATE DATABASE IF NOT EXISTS " + database;
connection.setAutoCommit(false);
statement.execute(createDb);
connection.commit();
log.info("创建数据库({})成功", database);
if (set.isClosed()) {
set.close();
}
return true;
}
set.close();
log.info("数据库已存在,无需初始化");
return false;
} catch (SQLException e2) {
throw new RuntimeException(e2);
}
}
上面的实现比较清晰了,首先是判断数据库是否存在,这里需要注意的就是,我们需要自己额创建db的连接,并执行相关库的判断、初始化sql执行。
为什么不直接使用spring.datasource.url来创建连接?
因为库不存在时,直接使用下面这个url进行连接会抛出异常
表初始化
表初始化,其实可以理解为项目启动后要执行的一些sql,这时主要借助就是initializer.setDatabasePopulator
核心知识点
虽然技术派新增了一个DbChangeSetLoader类来实现初始化sql的加载,但实际上,若你完全抛开Liquibase,单纯的希望项目启动后执行某些sql,可以非常简单的实现,直接用下面这种就可以了啦。
- 通过@Value来加载需要初始化的sql文件
- 直接通过ResourceDatabasePoplulator添加sql资源
Liquibase兼容方案
在技术派中,做了Liquibase的兼容,即找那些sql需要进行初始化,完全在遵循了Liquibase中定义的xml文件
要想要在技术派中使用这种方式进行初始化,如使用marizadb时,需要修改配置参数
spring.liquibase.enable:false
核心的实现如下:
对于liquibase的xml文件解析,核心逻辑在DbChangeSetLoader中,借助sax来进行xml文件的解析(Spring也是用sax解析xml的)
实现看源码,有两个知识点:
1.如何加载xml文件
下面的传参set是相对路径,如liquibase/data/init_data_221216.sql
2、sax的xml解析
小结
介绍项目启动之后库表的初始化操作,结合实际的代码介绍了两种使用姿势
- Liquibase:代表的数据库版本管理方式
- DataSourceInitializer:代表的项目启动之后执行某些初始化方式
更多关注:
DataSourceInitializer方式-数据库初始化方式