数据库版本控制liquibase
文章目录
一、liquibase介绍
1、什么是liquibase
liquibase是一个数据库变更的版本控制工具。项目中通过liquibase解析用户编写的liquibase的配置文件,生成sql语句,并执行和记录。执行是根据记录确定sql语句是否曾经执行过,和配置文件里的预判断语句确定sql是否执行。liquibase开源版使用Apache 2.0协议。
官网地址:https://www.liquibase.org/
2、liquibase的优点
1、配置文件支持SQL、XML、JSON 或者 YAML2、版本控制按序执行3、可以用上下文控制sql在何时何地如何执行。4、支持schmea的变更5、根据配置文件自动生成sql语句用于预览6、可重复执行迁移7、可插件拓展8、可回滚9、可兼容14种主流数据库如oracle,mysql,pg等,支持平滑迁移10、支持schema方式的多租户(multi-tenant)
3、liquibase的基本概念
1、changeSet 执行sql的并记录、版本控制的最小单元。即每条changeSet生成1条执行记录,版本控制是基于执行记录的。 2、changelog 即执行记录。由changeSet执行后产生的记录。记录默认保存在databasechangelog表中,此表由liquibase自动生成。包含id,author,filename,dateexcuted,orderexcuted,exectype,md5sum等字段。 3、databasechangeloglock。liquibase的锁表。liquibase在执行前更新此表的locked为true,执行完liquibase的工作,将locked更新为false,适合集群使用
4、官网地址
https://www.liquibase.org/
5、Liquibase目录结构
Liquibase的changelog文件放在resources/db/changelog目录下,如下图所示,我们只需要追加更改这些文件来更新数据库。db.changelog-master.xml是主文件,只用于包含文件,一般不做更改。根据修改类型:增加,修改,删除表对象和数据导入分别在各自文件添加。数据文件以csv格式放到csv目录下。
二、liquibase使用
1、数据库配置
# 数据源
spring:
datasource:
pig:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.postgresql.Driver
username: postgres
password: 123456
jdbc-url: jdbc:postgresql://${POSTGRE_HOST:pig-postgre}:${POSTGRE_PORT:5432}/${POSTGRE_DB:pig}?currentSchema=pig
liquibase:
enabled: true
change-log: classpath:/db/liquibase/db.changelog-master.xml
db.changelog-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.4.xsd">
<!-- 初始化数据库时候执行的 -->
<includeAll path="./init-sql/" relativeToChangelogFile="true"/>
<!-- sqlchangelog文件中存放迭代版本中产生的sql脚本-->
<includeAll path="./sqlchangelog/" relativeToChangelogFile="true"/>
</databaseChangeLog>
2、数据库产生表
CREATE TABLE "pig_config"."databasechangelog" (
"id" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
"author" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
"filename" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
"dateexecuted" timestamp(6) NOT NULL,
"orderexecuted" int4 NOT NULL,
"exectype" varchar(10) COLLATE "pg_catalog"."default" NOT NULL,
"md5sum" varchar(35) COLLATE "pg_catalog"."default",
"description" varchar(255) COLLATE "pg_catalog"."default",
"comments" varchar(255) COLLATE "pg_catalog"."default",
"tag" varchar(255) COLLATE "pg_catalog"."default",
"liquibase" varchar(20) COLLATE "pg_catalog"."default",
"contexts" varchar(255) COLLATE "pg_catalog"."default",
"labels" varchar(255) COLLATE "pg_catalog"."default",
"deployment_id" varchar(10) COLLATE "pg_catalog"."default"
)
;
ALTER TABLE "pig_config"."databasechangelog"
OWNER TO "cloud_dev_pg";
CREATE TABLE "pig_config"."databasechangeloglock" (
"id" int4 NOT NULL,
"locked" bool NOT NULL,
"lockgranted" timestamp(6),
"lockedby" varchar(255) COLLATE "pg_catalog"."default",
CONSTRAINT "databasechangeloglock_pkey" PRIMARY KEY ("id")
)
;
ALTER TABLE "pig_config"."databasechangeloglock"
OWNER TO "cloud_dev_pg";
3、文件配置
如下所示SQL方式:
--liquibase formatted sql
--changeset test:20230607_1
-- 2023年6月7日 崔辛福 告警通知规则新增组织名称代码
ALTER TABLE "aio_event"."rule_config" ADD COLUMN org_name varchar(100);
COMMENT ON COLUMN "aio_event"."rule_config"."org_name" is '机构昵称';
--changeset test:20230612_1
-- 2023年6月12日 崔辛福 值守报告推送新增日周月cron表达式
ALTER TABLE "aio_event"."report_push_set" ADD COLUMN day_cron varchar(50);
COMMENT ON COLUMN "aio_event"."report_push_set"."day_cron" is '日报接收时间表达式';
ALTER TABLE "aio_event"."report_push_set" ADD COLUMN week_cron varchar(50);
COMMENT ON COLUMN "aio_event"."report_push_set"."week_cron" is '周报接收时间表达式';
ALTER TABLE "aio_event"."report_push_set" ADD COLUMN month_cron varchar(50);
COMMENT ON COLUMN "aio_event"."report_push_set"."month_cron" is '月报接收时间表达式';
说明例如:
–changeset test:20230612_1
– 2023年6月7日 说明
具体说明:
changset 为一条记录标识
test作者信息
20230612_1 执行id
– 2023年6月7日 为用户自己添加的注释,不会参与入库和其他操作,只是纯粹的注释一下
如下所示XML方式:
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd
http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
<!-- ################### Table相关 ################### -->
<!-- 创建表 -->
<changeSet author="cavan" id="22.1.V1-1">
<preConditions onFail="MARK_RAN">
<not>
<tableExists tableName="school"/>
</not>
</preConditions>
<createTable tableName="school">
<!-- int类型 -->
<column name="school_id" type="INT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<!-- 字符串类型 -->
<column name="school_name" type="VARCHAR(100)">
<constraints nullable="true" unique="false"/>
</column>
<!-- 文本类型 -->
<column name="address" type="TEXT"/>
<!-- boolean类型 使用boolean或者tinyint(1)-->
<column name="is_top_ten" type="boolean" defaultValueBoolean="true"/>
<!-- 方式二
<column defaultValueNumeric="0" name="is_top_ten" type="tinyint(1)"/>
-->
<column defaultValue="anonymity" name="created_by" type="VARCHAR(50)"/>
<column defaultValueComputed="CURRENT_TIMESTAMP" name="created_date" type="TIMESTAMP">
<constraints nullable="false"/>
</column>
<column defaultValueComputed="CURRENT_TIMESTAMP" name="last_modified_date" type="TIMESTAMP">
<constraints nullable="false"/>
</column>
<column defaultValue="anonymity" name="last_modified_by" type="VARCHAR(50)"/>
</createTable>
</changeSet>
<changeSet author="cavan" id="22.1.V1-2">
<preConditions onFail="MARK_RAN">
<not>
<tableExists tableName="school_class"/>
</not>
</preConditions>
<createTable tableName="school_class">
<column name="class_id" type="INT">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="class_name" type="VARCHAR(100)">
<constraints nullable="true"/>
</column>
<column name="school_id" type="INT">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
<!-- 删除表 -->
<changeSet author="cavan" id="22.1.V1-3">
<preConditions>
<tableExists tableName="school_class" />
</preConditions>
<dropTable tableName="school_class" />
</changeSet>
<!-- 修改表名 -->
<changeSet author="cavan" id="22.1.V1-4">
<preConditions>
<tableExists tableName="school_class" />
</preConditions>
<renameTable oldTableName="school_class"
newTableName="class_school"/>
</changeSet>
<!-- ################### 其他相关表操作 ################### -->
<!-- 增加主键,单一主键 -->
<changeSet author="cavan" id="22.1.V2-1">
<addPrimaryKey columnNames="school_id"
constraintName="PRIMARY"
tableName="school"/>
</changeSet>
<!-- 增加主键,联合主键 -->
<changeSet author="cavan" id="22.1.V2-2">
<addPrimaryKey columnNames="school_id, school_name"
constraintName="PRIMARY"
tableName="school"/>
</changeSet>
<!-- 创建索引,删除索引 -->
<changeSet author="cavan" id="22.1.V2-3">
<!-- 创建索引 -->
<createIndex indexName="school_id" tableName="school">
<column name="school"/>
</createIndex>
<!-- 删除索引 -->
<dropIndex indexName="school_id" tableName="school"/>
</changeSet>
<!-- 增加外键约束 -->
<changeSet author="cavan" id="22.1.V2-4">
<addForeignKeyConstraint baseColumnNames="school_id"
baseTableName="school_class"
constraintName="school_class_ibfk_1"
deferrable="false"
initiallyDeferred="false"
onDelete="RESTRICT"
onUpdate="RESTRICT"
referencedColumnNames="school_id"
referencedTableName="school"/>
</changeSet>
<changeSet author="cavan" id="22.1.V2-5">
<sql>
ALTER TABLE school MODIFY COLUMN last_modified_date timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
</sql>
</changeSet>
<!-- ################### Column相关 ################### -->
<!-- 增加字段 -->
<changeSet author="cavan" id="22.1.V3-1">
<addColumn tableName="school_class">
<column name="school_name" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<!-- 删除字段 -->
<changeSet author="cavan" id="22.1.V3-2">
<dropColumn tableName="school">
<column name="created_by"/>
<column name="created_date"/>
<column name="last_modified_by"/>
<column name="last_modified_date"/>
</dropColumn>
</changeSet>
<!-- 修改字段 -->
<changeSet author="cavan" id="22.1.V3-3">
<!-- 修改字段名称( 其实可以连带类型一起修改了 ) -->
<renameColumn tableName="school" oldColumnName="is_top_ten"
newColumnName="is_top_ten_new" columnDataType="varchar(20)"/>
<!-- 修改字段类型 -->
<modifyDataType tableName="school" columnName="school_name" newDataType="varchar(20)" />
</changeSet>
<!-- ################### 数据相关 ################### -->
<!-- 增删改查数据 -->
<changeSet author="cavan" id="22.1.V4-1">
<insert tableName="school">
<column name="school_id" value="2"/>
<column name="school_name" value="qinghua"/>
<column name="created_by" value="anonymity"/>
<column name="created_date" valueDate="2021-07-20 15:51:53.0"/>
<column name="last_modified_date" valueDate="2021-07-20 15:51:53.0"/>
<column name="last_modified_by" value="anonymity"/>
</insert>
</changeSet>
<changeSet author="cavan" id="22.1.V4-2">
<delete tableName="school">
<where>school_id='2'</where>
</delete>
</changeSet>
<changeSet author="cavan" id="22.1.V4-3">
<update tableName="school">
<column name="school_name" value="beida"/>
<where>school_id='2'</where>
</update>
</changeSet>
<!-- 基于SQL语句 -->
<changeSet author="cavan" id="22.1.V5-1">
<sql>
insert into school (school_id, school_name, is_top_ten) values (1, 'hafu', 1);
</sql>
</changeSet>
<!-- 基于SQL文件 -->
<changeSet author="cavan" id="22.1.V5-2">
<sqlFile path="insert-data.sql"/>
</changeSet>
</databaseChangeLog>
4、使用方式代码
启动服务(AioDBApplication)连接的数据源会在数据库里面产生两张表:
databasechangelog
databasechangeloglock
● databaseChangeLog文件,Liquibase入口文件。这个文件里可以引入你的版本修改文件,执行顺序为从上到下。
● LiquiBase在执行changelog时,会在数据库中插入两张表:DATABASECHANGELOG和DATABASECHANGELOGLOCK,分别记录changelog的执行日志和锁日志。
● LiquiBase在执行changelog中的changeSet时,会首先查看DATABASECHANGELOG表,若是已经执行过,则会跳过(除非changeSet的runAlways属性为true),若是没有执行过,则执行并记录changelog日志;
● changelog中的一个changeSet对应一个事务,在changeSet执行完后commit,若是出现错误则rollback;
链接数据源代码:
DataSourceConfig.Java
import liquibase.integration.spring.SpringLiquibase;
import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
/**
* @author: test
* @createTime: 2023年05月31日 16:58
* @description: -
*/
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.aio")
public DataSource aioDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.aio.liquibase")
public LiquibaseProperties aioLiquibaseProperties() {
return new LiquibaseProperties();
}
@Bean
public SpringLiquibase aioLiquibase() {
return createSpringLiquibase(aioDataSource(), aioLiquibaseProperties());
}
private static SpringLiquibase createSpringLiquibase(DataSource dataSource, LiquibaseProperties properties) {
SpringLiquibase liquibase = new SpringLiquibase();
liquibase.setDataSource(dataSource);
liquibase.setChangeLog(properties.getChangeLog());
liquibase.setContexts(properties.getContexts());
liquibase.setDefaultSchema(properties.getDefaultSchema());
liquibase.setDropFirst(properties.isDropFirst());
liquibase.setShouldRun(properties.isEnabled());
liquibase.setLabels(properties.getLabels());
liquibase.setChangeLogParameters(properties.getParameters());
liquibase.setRollbackFile(properties.getRollbackFile());
return liquibase;
}
}
可以在程序跑完结束掉:
如下代码所示
在
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ExitCodeGenerator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.Environment;
import java.net.InetAddress;
/**
* @Author: wb_fb132
* @createTime: 2023年04月06日 15:37:19
* @Description: 数据库变更的版本控制服务
*/
@SpringBootApplication
class AioDBApplication {
private static final Logger logger = LoggerFactory.getLogger(AioDBApplication.class);
public static void main(String[] args) throws Exception {
SpringApplication app = new SpringApplication(AioDBApplication.class);
ConfigurableApplicationContext context = app.run(args);
Environment env = context.getEnvironment();
String protocol = "http";
if (env.getProperty("server.ssl.key-store") != null) {
protocol = "https";
}
logger.info("\n----------------------------------------------------------\n\t" +
"Application '{}' is running! Access URLs:\n\t" +
"Local: \t\t{}://localhost:{}\n\t" +
"External: \t{}://{}:{}\n\t" +
"Profile(s): \t{}\n----------------------------------------------------------",
env.getProperty("spring.application.name"),
protocol,
env.getProperty("server.port"),
protocol,
InetAddress.getLocalHost().getHostAddress(),
env.getProperty("server.port"),
env.getActiveProfiles());
// 服务停止
exitApplication(context);
}
public static void exitApplication(ConfigurableApplicationContext context) {
try {
Thread.sleep(3000);
int exitCode = SpringApplication.exit(context, (ExitCodeGenerator) () -> 0);
logger.info("\n----------------------------------------------------------\n\t" +
"Application '{}' is exit succeeded!\n\t" +
"ExitCode(s): \t{}\n----------------------------------------------------------",
"aio-cloud-db",
exitCode);
System.exit(exitCode);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}