fix: 码全代码添加

This commit is contained in:
2024-10-11 15:56:51 +08:00
parent 9a853f40c2
commit 7776e95902
907 changed files with 81296 additions and 324 deletions

View File

@@ -0,0 +1,38 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2023 codvision.com All Rights Reserved.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.codvision</groupId>
<artifactId>cvbp-public</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cvbp-db-core</artifactId>
<packaging>jar</packaging>
<version>1.0.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.codvision</groupId>
<artifactId>cvbp-common-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,15 @@
package com.codvision.db;
import com.codvision.db.constants.DbQueryProperty;
public interface DataSourceFactory {
/**
* 创建数据源实例
*
* @param property
* @return
*/
DbQuery createDbQuery(DbQueryProperty property);
}

View File

@@ -0,0 +1,83 @@
package com.codvision.db;
import com.codvision.db.core.DbColumn;
import com.codvision.db.core.DbTable;
import org.springframework.jdbc.core.RowMapper;
/**
* 表数据查询接口
*
* @author yuwei
* @since 2020-03-14
*/
public interface DbDialect {
RowMapper<DbTable> tableMapper();
RowMapper<DbColumn> columnMapper();
RowMapper<String> viewMapper();
/**
* 获取指定表的所有列
*
* @param dbName
* @param tableName
* @return
*/
String columns(String dbName, String tableName);
/**
* 获取数据库下的 所有表
*
* @param dbName
* @return
*/
String tables(String dbName);
/**
* 获取表结构
*
* @param dbName
* @param tableName
* @return
*/
String table(String dbName, String tableName);
/**
* 获取数据库下的 所有视图
*
* @param dbName
* @return
*/
String views(String dbName);
/**
* 构建 分页 sql
*
* @param sql
* @param offset
* @param count
* @return
*/
String buildPaginationSql(String sql, long offset, long count);
/**
* 包装 count sql
*
* @param sql
* @return
*/
String count(String sql);
/**
* oracl 读取long 类型会流关闭是oracle的bug需要特殊处理
*
* @return
*/
default RowMapper<DbColumn> columnLongMapper() {
return null;
}
;
}

View File

@@ -0,0 +1,148 @@
package com.codvision.db;
import com.codvision.db.core.DbColumn;
import com.codvision.db.core.DbTable;
import com.codvision.db.core.PageResult;
import java.sql.Connection;
import java.util.List;
import java.util.Map;
/**
* 表数据查询接口
*
* @author yuwei
* @since 2020-03-14
*/
public interface DbQuery {
/**
* 获取数据库连接
*/
Connection getConnection();
/**
* 检测连通性
*/
boolean valid();
/**
* 关闭数据源
*/
void close();
/**
* 获取指定表 具有的所有字段列表
*
* @param dbName
* @param tableName
* @return
*/
List<DbColumn> getTableColumns(String dbName, String tableName);
/**
* 获取指定数据库下 所有的表信息
*
* @param dbName
* @return
*/
List<DbTable> getTables(String dbName);
/**
* 获取指定数据库下 指定表信息
*
* @param dbName
* @param tableName
* @return
*/
DbTable getTable(String dbName, String tableName);
/**
* 获取数据库下的所有schemas
*
* @return
*/
List<String> getSchemas();
/**
* 获取数据库下的所有视图
*
* @param dbName 数据库名称
* @return
*/
List<String> getViews(String dbName);
/**
* 获取总数
*
* @param sql
* @return
*/
int count(String sql);
/**
* 获取总数带查询参数
*
* @param sql
* @return
*/
int count(String sql, Object[] args);
/**
* 获取总数带查询参数 NamedParameterJdbcTemplate
*
* @param sql
* @return
*/
int count(String sql, Map<String, Object> params);
/**
* 查询结果列表
*
* @param sql
* @return
*/
List<Map<String, Object>> queryList(String sql);
/**
* 查询结果列表带查询参数
*
* @param sql
* @param args
* @return
*/
List<Map<String, Object>> queryList(String sql, Object[] args);
/**
* 查询结果分页
*
* @param sql
* @param offset
* @param size
* @return
*/
PageResult<Map<String, Object>> queryByPage(String sql, long offset, long size);
/**
* 查询结果分页带查询参数
*
* @param sql
* @param args
* @param offset
* @param size
* @return
*/
PageResult<Map<String, Object>> queryByPage(String sql, Object[] args, long offset, long size);
/**
* 查询结果分页带查询参数 NamedParameterJdbcTemplate
*
* @param sql
* @param params
* @param offset
* @param size
* @return
*/
PageResult<Map<String, Object>> queryByPage(String sql, Map<String, Object> params, long offset, long size);
}

View File

@@ -0,0 +1,20 @@
package com.codvision.db;
import com.codvision.db.constants.DbType;
import com.codvision.db.dialect.DialectRegistry;
/**
* 方言工厂类
*
* @author yuwei
* @since 2020-03-14
*/
public class DialectFactory {
private static final DialectRegistry DIALECT_REGISTRY = new DialectRegistry();
public static DbDialect getDialect(DbType dbType) {
return DIALECT_REGISTRY.getDialect(dbType);
}
}

View File

@@ -0,0 +1,123 @@
package com.codvision.db.cache;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class DefaultSqlCache extends LinkedHashMap<String, DefaultSqlCache.ExpireNode<Object>> implements SqlCache {
private int capacity;
private long expire;
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public DefaultSqlCache(int capacity, long expire) {
super((int) Math.ceil(capacity / 0.75) + 1, 0.75f, true);
// 容量
this.capacity = capacity;
// 固定过期时间
this.expire = expire;
}
@Override
public void put(String key, Object value, long ttl) {
long expireTime = Long.MAX_VALUE;
if (ttl >= 0) {
expireTime = System.currentTimeMillis() + (ttl == 0 ? this.expire : ttl);
}
lock.writeLock().lock();
try {
// 封装成过期时间节点
put(key, new ExpireNode<>(expireTime, value));
} finally {
lock.writeLock().unlock();
}
}
@Override
public Object get(String key) {
lock.readLock().lock();
ExpireNode<Object> expireNode;
try {
expireNode = super.get(key);
} finally {
lock.readLock().unlock();
}
if (expireNode == null) {
return null;
}
// 惰性删除过期的
if (this.expire > -1L && expireNode.expire < System.currentTimeMillis()) {
try {
lock.writeLock().lock();
super.remove(key);
} finally {
lock.writeLock().unlock();
}
return null;
}
return expireNode.value;
}
@Override
public void delete(String key) {
try {
lock.writeLock().lock();
Iterator<Map.Entry<String, ExpireNode<Object>>> iterator = super.entrySet().iterator();
// 清除key的缓存
while (iterator.hasNext()) {
Map.Entry<String, ExpireNode<Object>> entry = iterator.next();
if (entry.getKey().equals(key)) {
iterator.remove();
}
}
} finally {
lock.writeLock().unlock();
}
}
@Override
protected boolean removeEldestEntry(Map.Entry<String, ExpireNode<Object>> eldest) {
if (this.expire > -1L && size() > capacity) {
clean();
}
// lru淘汰
return size() > this.capacity;
}
/**
* 清理已过期的数据
*/
private void clean() {
try {
lock.writeLock().lock();
Iterator<Map.Entry<String, ExpireNode<Object>>> iterator = super.entrySet().iterator();
long now = System.currentTimeMillis();
while (iterator.hasNext()) {
Map.Entry<String, ExpireNode<Object>> next = iterator.next();
// 判断是否过期
if (next.getValue().expire < now) {
iterator.remove();
}
}
} finally {
lock.writeLock().unlock();
}
}
/**
* 过期时间节点
*/
static class ExpireNode<V> {
long expire;
Object value;
public ExpireNode(long expire, Object value) {
this.expire = expire;
this.value = value;
}
}
}

View File

@@ -0,0 +1,41 @@
package com.codvision.db.cache;
import com.codvision.commoncore.utils.Md5Utils;
import java.util.Arrays;
/**
* SQL缓存接口
*/
public interface SqlCache {
/**
* 计算key
*/
default String buildSqlCacheKey(String sql, Object[] args) {
return Md5Utils.getMd5(sql + ":" + Arrays.toString(args));
}
/**
* 存入缓存
*
* @param key key
* @param value 值
*/
void put(String key, Object value, long ttl);
/**
* 获取缓存
*
* @param key key
* @return
*/
<T> T get(String key);
/**
* 删除缓存
*
* @param key key
*/
void delete(String key);
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright (c) 2023 codvision.com All Rights Reserved.
*/
package com.codvision.db.config;
import com.codvision.db.versionctl.DbVersionCtlInitializer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 数据源模块配置
*
* @author lingee
* @date 2023/12/5
*/
@Slf4j
@Configuration
@ImportAutoConfiguration(DBVCTLProps.class)
public class DBConfig {
@Bean("dbVersionCtlInitializer")
public DbVersionCtlInitializer init() {
return new DbVersionCtlInitializer();
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (c) 2023 codvision.com All Rights Reserved.
*/
package com.codvision.db.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 数据版本配置控制
*
* @author lingee
* @date 2023/12/5
*/
@Data
@Configuration
@ConfigurationProperties("cvbp.dbvctl")
public class DBVCTLProps {
/**
* 是否自动执行数据库版本控制,默认不执行
*/
private Boolean enabled = Boolean.FALSE;
/**
* 数据库非空但首次使用数据库版本管理时,指定生成版本基线的业务空间及其基线版本,多个业务空间时使用逗号连接。
* 例如:"raven_V1.0.0,sentry_V1.1.2"
*/
private String baselineBusinessSpaceAndVersions;
/**
* 是否重置数据库基线版本
*/
private Boolean baselineReset = Boolean.FALSE;
/**
* 数据库基线版本重置条件SQL只有[baselineReset]设置为true且该SQL查询结果非空才会进行数据库基线版本重置操作
* 通常建议使用时间戳字段[install_time]作为查询SQL的条件这样只会生效一次
* 以后升级版本时即使忘记将【baselineReset】属性清除或设置为false也不会导致数据库基线版本被误重置。
*/
private String baselineResetConditionSql = "";
}

View File

@@ -0,0 +1,38 @@
package com.codvision.db.constants;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.codvision.db.exception.DataQueryException;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.io.Serializable;
@Data
@AllArgsConstructor
public class DbQueryProperty implements Serializable {
private static final long serialVersionUID = 1L;
private DbType dbType;
private String host;
private String username;
private String password;
private Integer port;
private String dbName;
private String sid;
/**
* 参数合法性校验
*/
public void valid() {
if (ObjectUtil.isEmpty(dbType) || StrUtil.isEmpty(host) ||
StrUtil.isEmpty(username) || StrUtil.isEmpty(password) ||
ObjectUtil.isEmpty(port)) {
throw new DataQueryException("参数不完整");
}
if (DbType.OTHER.getDb().equals(dbType)) {
throw new DataQueryException("不支持的数据库类型");
}
}
}

View File

@@ -0,0 +1,90 @@
package com.codvision.db.constants;
/**
* 数据库类型
*
* @author yuwei
* @since 2020-03-14
*/
public enum DbType {
/**
* MYSQL
*/
MYSQL("1", "MySql数据库", "jdbc:mysql://${host}:${port}/${dbName}?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8"),
/**
* MARIADB
*/
MARIADB("2", "MariaDB数据库", "jdbc:mariadb://${host}:${port}/${dbName}"),
/**
* ORACLE
*/
ORACLE("3", "Oracle11g及以下数据库", "jdbc:oracle:thin:@${host}:${port}:${sid}"),
/**
* oracle12c new pagination
*/
ORACLE_12C("4", "Oracle12c+数据库", "jdbc:oracle:thin:@${host}:${port}:${sid}"),
/**
* POSTGRESQL
*/
POSTGRESQL("5", "PostgreSql数据库", "jdbc:postgresql://${host}:${port}/${dbName}"),
/**
* SQLSERVER2005
*/
MSSQL2008("6", "SQLServer2008及以下数据库", "jdbc:sqlserver://${host}:${port};DatabaseName=${dbName}"),
/**
* SQLSERVER
*/
MSSQL("7", "SQLServer2012+数据库", "jdbc:sqlserver://${host}:${port};DatabaseName=${dbName}"),
/**
* UNKONWN DB
*/
OTHER("8", "其他数据库", "");
/**
* 数据库名称
*/
private final String db;
/**
* 描述
*/
private final String desc;
/**
* url
*/
private final String url;
public String getDb() {
return this.db;
}
public String getDesc() {
return this.desc;
}
public String getUrl() {
return this.url;
}
DbType(String db, String desc, String url) {
this.db = db;
this.desc = desc;
this.url = url;
}
/**
* 获取数据库类型
*
* @param dbType 数据库类型字符串
*/
public static DbType getDbType(String dbType) {
for (DbType type : DbType.values()) {
if (type.name().equals(dbType)) {
return type;
}
}
return OTHER;
}
}

View File

@@ -0,0 +1,57 @@
package com.codvision.db.core;
import lombok.Data;
@Data
public class DbColumn {
/**
* 列名
*/
private String colName;
/**
* 数据类型
*/
private String dataType;
/**
* 数据长度
*/
private String dataLength;
/**
* 数据精度
*/
private String dataPrecision;
/**
* 数据小数位
*/
private String dataScale;
/**
* 是否主键
*/
private Boolean colKey;
/**
* 是否允许为空
*/
private Boolean nullable;
/**
* 列的序号
*/
private Integer colPosition;
/**
* 列默认值
*/
private String dataDefault;
/**
* 列注释
*/
private String colComment;
}

View File

@@ -0,0 +1,17 @@
package com.codvision.db.core;
import lombok.Data;
@Data
public class DbTable {
/**
* 表名
*/
private String tableName;
/**
* 表注释
*/
private String tableComment;
}

View File

@@ -0,0 +1,24 @@
package com.codvision.db.core;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.List;
@Data
@Accessors(chain = true)
public class PageResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
private Integer pageNum;
private Integer pageSize;
private Integer total;
private List<T> data;
public PageResult(Integer total, List<T> data) {
this.total = total;
this.data = data;
}
}

View File

@@ -0,0 +1,62 @@
package com.codvision.db.datasource;
import com.codvision.db.DataSourceFactory;
import com.codvision.db.DbDialect;
import com.codvision.db.DbQuery;
import com.codvision.db.DialectFactory;
import com.codvision.db.constants.DbQueryProperty;
import com.codvision.db.constants.DbType;
import com.codvision.db.exception.DataQueryException;
import com.codvision.db.query.AbstractDbQueryFactory;
import com.codvision.db.query.CacheDbQueryFactoryBean;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
public abstract class AbstractDataSourceFactory implements DataSourceFactory {
@Override
public DbQuery createDbQuery(DbQueryProperty property) {
property.valid();
DataSource dataSource = createDataSource(property);
DbQuery dbQuery = createDbQuery(dataSource, property.getDbType());
return dbQuery;
}
public DbQuery createDbQuery(DataSource dataSource, DbType dbType) {
DbDialect dbDialect = DialectFactory.getDialect(dbType);
if (dbDialect == null) {
throw new DataQueryException("该数据库类型正在开发中");
}
AbstractDbQueryFactory dbQuery = new CacheDbQueryFactoryBean();
dbQuery.setDataSource(dataSource);
dbQuery.setJdbcTemplate(new JdbcTemplate(dataSource));
dbQuery.setDbDialect(dbDialect);
return dbQuery;
}
public DataSource createDataSource(DbQueryProperty property) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(trainToJdbcUrl(property));
dataSource.setUsername(property.getUsername());
dataSource.setPassword(property.getPassword());
return dataSource;
}
protected String trainToJdbcUrl(DbQueryProperty property) {
String url = property.getDbType().getUrl();
if (StringUtils.isEmpty(url)) {
throw new DataQueryException("无效数据库类型!");
}
url = url.replace("${host}", property.getHost());
url = url.replace("${port}", String.valueOf(property.getPort()));
if (DbType.ORACLE.equals(property.getDbType()) || DbType.ORACLE_12C.equals(property.getDbType())) {
url = url.replace("${sid}", property.getSid());
} else {
url = url.replace("${dbName}", property.getDbName());
}
return url;
}
}

View File

@@ -0,0 +1,65 @@
package com.codvision.db.datasource;
import com.codvision.db.constants.DbQueryProperty;
import javax.sql.DataSource;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class CacheDataSourceFactoryBean extends AbstractDataSourceFactory {
/**
* 数据源缓存
*/
private static Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<>();
@Override
public DataSource createDataSource(DbQueryProperty property) {
String key = property.getDbType() + ":" + property.getHost()
+ ":" + property.getPort() + ":" + property.getUsername()
+ ":" + property.getPassword() + ":" + property.getDbName()
+ ":" + property.getSid();
String s = compress(key);
DataSource dataSource = dataSourceMap.get(s);
if (null == dataSource) {
synchronized (CacheDataSourceFactoryBean.class) {
dataSource = super.createDataSource(property);
dataSourceMap.put(s, dataSource);
}
}
return dataSource;
}
// 压缩
public static String compress(String str) {
if (str == null || str.length() == 0) {
return str;
}
MessageDigest md = null;
try {
md = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
md.update(str.getBytes());
byte b[] = md.digest();
int i;
StringBuffer buf = new StringBuffer();
for (int offset = 0; offset < b.length; offset++) {
i = b[offset];
if (i < 0)
i += 256;
if (i < 16)
buf.append("0");
buf.append(Integer.toHexString(i));
}
// System.out.println("MD5(" + str + ",32小写) = " + buf.toString());
// System.out.println("MD5(" + str + ",32大写) = " + buf.toString().toUpperCase());
// System.out.println("MD5(" + str + ",16小写) = " + buf.toString().substring(8, 24));
// System.out.println("MD5(" + str + ",16大写) = " + buf.toString().substring(8, 24).toUpperCase());
return buf.toString().substring(8, 24).toUpperCase();
}
}

View File

@@ -0,0 +1,4 @@
package com.codvision.db.datasource;
public class DefaultDataSourceFactoryBean extends AbstractDataSourceFactory {
}

View File

@@ -0,0 +1,55 @@
package com.codvision.db.dialect;
import com.codvision.db.DbDialect;
import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
/**
* 方言抽象类
*
* @author yuwei
* @since 2020-03-14
*/
public abstract class AbstractDbDialect implements DbDialect {
@Override
public RowMapper<String> viewMapper() {
return (ResultSet rs, int rowNum) -> rs.getString("table_name");
}
@Override
public String columns(String dbName, String tableName) {
return "select column_name AS COLNAME, ordinal_position AS COLPOSITION, column_default AS DATADEFAULT, is_nullable AS NULLABLE, data_type AS DATATYPE, " +
"character_maximum_length AS DATALENGTH, numeric_precision AS DATAPRECISION, numeric_scale AS DATASCALE, column_key AS COLKEY, column_comment AS COLCOMMENT " +
"from information_schema.columns where table_schema = '" + dbName + "' and table_name = '" + tableName + "' order by ordinal_position ";
}
@Override
public String tables(String dbName) {
return "SELECT table_name AS TABLENAME, table_comment AS TABLECOMMENT FROM information_schema.tables where table_schema = '" + dbName + "' ";
}
public String table(String dbName, String tableName) {
return "SELECT table_name AS TABLENAME, table_comment AS TABLECOMMENT FROM information_schema.tables where table_schema = '" + dbName + "' and table_name = '" + tableName + "' ";
}
@Override
public String views(String dbName) {
return "SELECT table_name AS TABLENAME FROM information_schema.views where table_schema = '" + dbName + "' ";
}
@Override
public String buildPaginationSql(String originalSql, long offset, long count) {
// 获取 分页实际条数
StringBuilder sqlBuilder = new StringBuilder(originalSql);
sqlBuilder.append(" LIMIT ").append(offset).append(" , ").append(count);
return sqlBuilder.toString();
}
@Override
public String count(String sql) {
return "SELECT COUNT(*) FROM ( " + sql + " ) TEMP";
}
}

View File

@@ -0,0 +1,28 @@
package com.codvision.db.dialect;
import com.codvision.db.DbDialect;
import com.codvision.db.constants.DbType;
import java.util.EnumMap;
import java.util.Map;
public class DialectRegistry {
private final Map<DbType, DbDialect> dialect_enum_map = new EnumMap<>(DbType.class);
public DialectRegistry() {
dialect_enum_map.put(DbType.MARIADB, new MariaDBDialect());
dialect_enum_map.put(DbType.MYSQL, new MySqlDialect());
dialect_enum_map.put(DbType.ORACLE_12C, new Oracle12cDialect());
dialect_enum_map.put(DbType.ORACLE, new OracleDialect());
dialect_enum_map.put(DbType.POSTGRESQL, new PostgreDialect());
dialect_enum_map.put(DbType.MSSQL2008, new SQLServer2008Dialect());
dialect_enum_map.put(DbType.MSSQL, new SQLServerDialect());
dialect_enum_map.put(DbType.OTHER, new UnknownDialect());
}
public DbDialect getDialect(DbType dbType) {
return dialect_enum_map.get(dbType);
}
}

View File

@@ -0,0 +1,10 @@
package com.codvision.db.dialect;
/**
* MariaDB 数据库方言
*
* @author yuwei
* @since 2020-03-14
*/
public class MariaDBDialect extends MySqlDialect {
}

View File

@@ -0,0 +1,44 @@
package com.codvision.db.dialect;
import com.codvision.db.core.DbColumn;
import com.codvision.db.core.DbTable;
import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
/**
* MySql 数据库方言
*
* @author yuwei
* @since 2020-03-14
*/
public class MySqlDialect extends AbstractDbDialect {
@Override
public RowMapper<DbColumn> columnMapper() {
return (ResultSet rs, int rowNum) -> {
DbColumn entity = new DbColumn();
entity.setColName(rs.getString("COLNAME"));
entity.setDataType(rs.getString("DATATYPE"));
entity.setDataLength(rs.getString("DATALENGTH"));
entity.setDataPrecision(rs.getString("DATAPRECISION"));
entity.setDataScale(rs.getString("DATASCALE"));
entity.setColKey("PRI".equals(rs.getString("COLKEY")));
entity.setNullable("YES".equals(rs.getString("NULLABLE")));
entity.setColPosition(rs.getInt("COLPOSITION"));
entity.setDataDefault(rs.getString("DATADEFAULT"));
entity.setColComment(rs.getString("COLCOMMENT"));
return entity;
};
}
@Override
public RowMapper<DbTable> tableMapper() {
return (ResultSet rs, int rowNum) -> {
DbTable entity = new DbTable();
entity.setTableName(rs.getString("TABLENAME"));
entity.setTableComment(rs.getString("TABLECOMMENT"));
return entity;
};
}
}

View File

@@ -0,0 +1,17 @@
package com.codvision.db.dialect;
/**
* ORACLE Oracle12c+数据库方言
*
* @author yuwei
* @since 2020-03-14
*/
public class Oracle12cDialect extends OracleDialect {
@Override
public String buildPaginationSql(String originalSql, long offset, long count) {
StringBuilder sqlBuilder = new StringBuilder(originalSql);
sqlBuilder.append(" OFFSET ").append(offset).append(" ROWS FETCH NEXT ").append(count).append(" ROWS ONLY ");
return sqlBuilder.toString();
}
}

View File

@@ -0,0 +1,88 @@
package com.codvision.db.dialect;
import com.codvision.db.core.DbColumn;
import com.codvision.db.core.DbTable;
import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
/**
* Oracle Oracle11g及以下数据库方言
*
* @author yuwei
* @since 2020-03-14
*/
public class OracleDialect extends AbstractDbDialect {
@Override
public String columns(String dbName, String tableName) {
return "select columns.column_name AS colName, columns.data_type AS DATATYPE, columns.data_length AS DATALENGTH, columns.data_precision AS DATAPRECISION, " +
"columns.data_scale AS DATASCALE, columns.nullable AS NULLABLE, columns.column_id AS COLPOSITION, columns.data_default AS DATADEFAULT, comments.comments AS COLCOMMENT," +
"case when t.column_name is null then 0 else 1 end as COLKEY " +
"from sys.user_tab_columns columns LEFT JOIN sys.user_col_comments comments ON columns.table_name = comments.table_name AND columns.column_name = comments.column_name " +
"left join ( " +
"select col.column_name as column_name, con.table_name as table_name from user_constraints con, user_cons_columns col " +
"where con.constraint_name = col.constraint_name and con.constraint_type = 'P' " +
") t on t.table_name = columns.table_name and columns.column_name = t.column_name " +
"where columns.table_name = UPPER('" + tableName + "') order by columns.column_id ";
}
@Override
public String tables(String dbName) {
return "select tables.table_name AS TABLENAME, comments.comments AS TABLECOMMENT from sys.user_tables tables " +
"LEFT JOIN sys.user_tab_comments comments ON tables.table_name = comments.table_name ";
}
@Override
public String table(String dbName, String tableName) {
return "select tables.table_name AS TABLENAME, comments.comments AS TABLECOMMENT from sys.user_tables tables " +
"LEFT JOIN sys.user_tab_comments comments ON tables.table_name = comments.table_name where tables.table_name = '" + tableName + "' ";
}
@Override
public String buildPaginationSql(String originalSql, long offset, long count) {
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder.append("SELECT * FROM ( SELECT TMP.*, ROWNUM ROW_ID FROM ( ");
sqlBuilder.append(originalSql).append(" ) TMP WHERE ROWNUM <=").append((offset >= 1) ? (offset + count) : count);
sqlBuilder.append(") WHERE ROW_ID > ").append(offset);
return sqlBuilder.toString();
}
@Override
public RowMapper<DbColumn> columnLongMapper() {
return (ResultSet rs, int rowNum) -> {
DbColumn entity = new DbColumn();
entity.setDataDefault(rs.getString("DATADEFAULT"));
return entity;
};
}
@Override
public RowMapper<DbColumn> columnMapper() {
return (ResultSet rs, int rowNum) -> {
DbColumn entity = new DbColumn();
entity.setColName(rs.getString("COLNAME"));
entity.setDataType(rs.getString("DATATYPE"));
entity.setDataLength(rs.getString("DATALENGTH"));
entity.setDataPrecision(rs.getString("DATAPRECISION"));
entity.setDataScale(rs.getString("DATASCALE"));
entity.setColKey("1".equals(rs.getString("COLKEY")));
entity.setNullable("Y".equals(rs.getString("NULLABLE")));
//long类型单独处理
//entity.setDataDefault(rs.getString("DATADEFAULT"));
entity.setColPosition(rs.getInt("COLPOSITION"));
entity.setColComment(rs.getString("COLCOMMENT"));
return entity;
};
}
@Override
public RowMapper<DbTable> tableMapper() {
return (ResultSet rs, int rowNum) -> {
DbTable entity = new DbTable();
entity.setTableName(rs.getString("TABLENAME"));
entity.setTableComment(rs.getString("TABLECOMMENT"));
return entity;
};
}
}

View File

@@ -0,0 +1,78 @@
package com.codvision.db.dialect;
import com.codvision.db.core.DbColumn;
import com.codvision.db.core.DbTable;
import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
/**
* Postgre 数据库方言
*
* @author yuwei
* @since 2020-03-14
*/
public class PostgreDialect extends AbstractDbDialect {
@Override
public String columns(String dbName, String tableName) {
return "select col.column_name AS COLNAME, col.ordinal_position AS COLPOSITION, col.column_default AS DATADEFAULT, col.is_nullable AS NULLABLE, col.udt_name AS DATATYPE, " +
"col.character_maximum_length AS DATALENGTH, col.numeric_precision AS DATAPRECISION, col.numeric_scale AS DATASCALE, des.description AS COLCOMMENT, " +
"case when t.colname is null then 0 else 1 end as COLKEY " +
"from information_schema.columns col left join pg_description des on col.table_name::regclass = des.objoid and col.ordinal_position = des.objsubid " +
"left join ( " +
"select pg_attribute.attname as colname from pg_constraint inner join pg_class on pg_constraint.conrelid = pg_class.oid " +
"inner join pg_attribute on pg_attribute.attrelid = pg_class.oid and pg_attribute.attnum = any(pg_constraint.conkey) " +
"where pg_class.relname = '" + tableName + "' and pg_constraint.contype = 'p' " +
") t on t.colname = col.column_name " +
"where col.table_catalog = '" + dbName + "' and col.table_schema = 'public' and col.table_name = '" + tableName + "' order by col.ordinal_position ";
}
@Override
public String tables(String dbName) {
return "select relname AS TABLENAME, cast(obj_description(relfilenode, 'pg_class') as varchar) AS TABLECOMMENT from pg_class " +
"where relname in (select tablename from pg_tables where schemaname = 'public' and position('_2' in tablename) = 0) ";
}
@Override
public String table(String dbName, String tableName) {
return "select relname AS TABLENAME, cast(obj_description(relfilenode, 'pg_class') as varchar) AS TABLECOMMENT from pg_class " +
"where relname in (select tablename from pg_tables where schemaname = 'public' and tablename = '" + tableName + "'" +
" and position('_2' in tablename) = 0) ";
}
@Override
public String buildPaginationSql(String originalSql, long offset, long count) {
StringBuilder sqlBuilder = new StringBuilder(originalSql);
sqlBuilder.append(" LIMIT ").append(count).append(" offset ").append(offset);
return sqlBuilder.toString();
}
@Override
public RowMapper<DbColumn> columnMapper() {
return (ResultSet rs, int rowNum) -> {
DbColumn entity = new DbColumn();
entity.setColName(rs.getString("COLNAME"));
entity.setDataType(rs.getString("DATATYPE"));
entity.setDataLength(rs.getString("DATALENGTH"));
entity.setDataPrecision(rs.getString("DATAPRECISION"));
entity.setDataScale(rs.getString("DATASCALE"));
entity.setColKey("1".equals(rs.getString("COLKEY")) ? true : false);
entity.setNullable("YES".equals(rs.getString("NULLABLE")) ? true : false);
entity.setColPosition(rs.getInt("COLPOSITION"));
entity.setDataDefault(rs.getString("DATADEFAULT"));
entity.setColComment(rs.getString("COLCOMMENT"));
return entity;
};
}
@Override
public RowMapper<DbTable> tableMapper() {
return (ResultSet rs, int rowNum) -> {
DbTable entity = new DbTable();
entity.setTableName(rs.getString("TABLENAME"));
entity.setTableComment(rs.getString("TABLECOMMENT"));
return entity;
};
}
}

View File

@@ -0,0 +1,113 @@
package com.codvision.db.dialect;
import com.codvision.db.core.DbColumn;
import com.codvision.db.core.DbTable;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.util.StringUtils;
import java.sql.ResultSet;
/**
* SQLServer 2005 数据库方言
*
* @author yuwei
* @since 2020-03-14
*/
public class SQLServer2008Dialect extends AbstractDbDialect {
@Override
public String columns(String dbName, String tableName) {
return "select columns.name AS colName, columns.column_id AS COLPOSITION, columns.max_length AS DATALENGTH, columns.precision AS DATAPRECISION, columns.scale AS DATASCALE, " +
"columns.is_nullable AS NULLABLE, types.name AS DATATYPE, CAST(ep.value AS NVARCHAR(128)) AS COLCOMMENT, e.text AS DATADEFAULT, " +
"(select top 1 ind.is_primary_key from sys.index_columns ic left join sys.indexes ind on ic.object_id = ind.object_id and ic.index_id = ind.index_id and ind.name like 'PK_%' where ic.object_id=columns.object_id and ic.column_id=columns.column_id) AS COLKEY " +
"from sys.columns columns LEFT JOIN sys.types types ON columns.system_type_id = types.system_type_id " +
"LEFT JOIN syscomments e ON columns.default_object_id= e.id " +
"LEFT JOIN sys.extended_properties ep ON ep.major_id = columns.object_id AND ep.minor_id = columns.column_id AND ep.name = 'MS_Description' " +
"where columns.object_id = object_id('" + tableName + "') order by columns.column_id ";
}
@Override
public String tables(String dbName) {
return "select tables.name AS TABLENAME, CAST(ep.value AS NVARCHAR(128)) AS TABLECOMMENT " +
"from sys.tables tables LEFT JOIN sys.extended_properties ep ON ep.major_id = tables.object_id AND ep.minor_id = 0";
}
@Override
public String table(String dbName, String tableName) {
return "select tables.name AS TABLENAME, CAST(ep.value AS NVARCHAR(128)) AS TABLECOMMENT " +
"from sys.tables tables LEFT JOIN sys.extended_properties ep ON ep.major_id = tables.object_id AND ep.minor_id = 0" +
"where tables.name = '" + tableName + "'";
}
private static String getOrderByPart(String sql) {
String loweredString = sql.toLowerCase();
int orderByIndex = loweredString.indexOf("order by");
if (orderByIndex != -1) {
return sql.substring(orderByIndex);
} else {
return "";
}
}
@Override
public String buildPaginationSql(String originalSql, long offset, long count) {
StringBuilder pagingBuilder = new StringBuilder();
String orderby = getOrderByPart(originalSql);
String distinctStr = "";
String loweredString = originalSql.toLowerCase();
String sqlPartString = originalSql;
if (loweredString.trim().startsWith("select")) {
int index = 6;
if (loweredString.startsWith("select distinct")) {
distinctStr = "DISTINCT ";
index = 15;
}
sqlPartString = sqlPartString.substring(index);
}
pagingBuilder.append(sqlPartString);
// if no ORDER BY is specified use fake ORDER BY field to avoid errors
if (StringUtils.isEmpty(orderby)) {
orderby = "ORDER BY CURRENT_TIMESTAMP";
}
StringBuilder sql = new StringBuilder();
sql.append("WITH selectTemp AS (SELECT ").append(distinctStr).append("TOP 100 PERCENT ")
.append(" ROW_NUMBER() OVER (").append(orderby).append(") as __row_number__, ").append(pagingBuilder)
.append(") SELECT * FROM selectTemp WHERE __row_number__ BETWEEN ")
//FIX#299原因mysql中limit 10(offset,size) 是从第10开始不包含10,而这里用的BETWEEN是两边都包含所以改为offset+1
.append(offset + 1)
.append(" AND ")
.append(offset + count).append(" ORDER BY __row_number__");
return sql.toString();
}
@Override
public RowMapper<DbColumn> columnMapper() {
return (ResultSet rs, int rowNum) -> {
DbColumn entity = new DbColumn();
entity.setColName(rs.getString("COLNAME"));
entity.setDataType(rs.getString("DATATYPE"));
entity.setDataLength(rs.getString("DATALENGTH"));
entity.setDataPrecision(rs.getString("DATAPRECISION"));
entity.setDataScale(rs.getString("DATASCALE"));
entity.setColKey("1".equals(rs.getString("COLKEY")) ? true : false);
entity.setNullable("1".equals(rs.getString("NULLABLE")) ? true : false);
entity.setColPosition(rs.getInt("COLPOSITION"));
entity.setDataDefault(rs.getString("DATADEFAULT"));
entity.setColComment(rs.getString("COLCOMMENT"));
return entity;
};
}
@Override
public RowMapper<DbTable> tableMapper() {
return (ResultSet rs, int rowNum) -> {
DbTable entity = new DbTable();
entity.setTableName(rs.getString("TABLENAME"));
entity.setTableComment(rs.getString("TABLECOMMENT"));
return entity;
};
}
}

View File

@@ -0,0 +1,17 @@
package com.codvision.db.dialect;
/**
* SQLServer 数据库方言
*
* @author yuwei
* @since 2020-03-14
*/
public class SQLServerDialect extends SQLServer2008Dialect {
@Override
public String buildPaginationSql(String originalSql, long offset, long count) {
StringBuilder sqlBuilder = new StringBuilder(originalSql);
sqlBuilder.append(" OFFSET ").append(offset).append(" ROWS FETCH NEXT ").append(count).append(" ROWS ONLY ");
return sqlBuilder.toString();
}
}

View File

@@ -0,0 +1,50 @@
package com.codvision.db.dialect;
import com.codvision.db.core.DbColumn;
import com.codvision.db.core.DbTable;
import com.codvision.db.exception.DataQueryException;
import org.springframework.jdbc.core.RowMapper;
/**
* 未知 数据库方言
*
* @author yuwei
* @since 2020-03-14
*/
public class UnknownDialect extends AbstractDbDialect {
@Override
public String columns(String dbName, String tableName) {
throw new DataQueryException("不支持的数据库类型");
}
@Override
public String tables(String dbName) {
throw new DataQueryException("不支持的数据库类型");
}
@Override
public String table(String dbName, String tableName) {
throw new DataQueryException("不支持的数据库类型");
}
@Override
public String buildPaginationSql(String sql, long offset, long count) {
throw new DataQueryException("不支持的数据库类型");
}
@Override
public String count(String sql) {
throw new DataQueryException("不支持的数据库类型");
}
@Override
public RowMapper<DbColumn> columnMapper() {
throw new DataQueryException("不支持的数据库类型");
}
@Override
public RowMapper<DbTable> tableMapper() {
throw new DataQueryException("不支持的数据库类型");
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2023 codvision.com All Rights Reserved.
*/
package com.codvision.db.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 数据库类型
*
* @author lingee
* @date 2023/12/5
*/
@Getter
@AllArgsConstructor
public enum DbTypeEnum {
/**
* mysql数据库
*/
mysql("mysql"),
/**
* pgsql数据库
*/
postgresql("postgresql");
private final String type;
public static DbTypeEnum resolve(String type) {
for (DbTypeEnum value : values()) {
if (value.getType().equals(type)) {
return value;
}
}
throw new EnumConstantNotPresentException(DbTypeEnum.class, type);
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright (c) 2023 codvision.com All Rights Reserved.
*/
package com.codvision.db.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 系统业务空间
*
* @author lingee
* @date 2023/12/12
*/
@AllArgsConstructor
@Getter
public enum SysBusinessSpace {
/**
* 基础能力中心
*/
SYS_BASE("base"),
/**
* 监控中心
*/
SYS_MONITOR("monitor"),
/**
* 认证中心
*/
SYS_AUTH("auth");
private final String code;
public static SysBusinessSpace resolve(String key) {
for (SysBusinessSpace value : values()) {
if (value.code.equals(key)) {
return value;
}
}
return null;
}
}

View File

@@ -0,0 +1,8 @@
package com.codvision.db.exception;
public class DataQueryException extends RuntimeException {
public DataQueryException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,11 @@
/*
* Copyright (c) 2023 codvision.com All Rights Reserved.
*/
/**
* 数据源核心模块
*
* @author lingee
* @date 2023/12/4
*/
package com.codvision.db;

View File

@@ -0,0 +1,181 @@
package com.codvision.db.query;
import com.codvision.db.DbDialect;
import com.codvision.db.DbQuery;
import com.codvision.db.core.DbColumn;
import com.codvision.db.core.DbTable;
import com.codvision.db.core.PageResult;
import com.codvision.db.dialect.OracleDialect;
import com.codvision.db.exception.DataQueryException;
import com.zaxxer.hikari.HikariDataSource;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@Setter
public abstract class AbstractDbQueryFactory implements DbQuery {
protected DataSource dataSource;
protected JdbcTemplate jdbcTemplate;
protected DbDialect dbDialect;
@Override
public Connection getConnection() {
try {
return dataSource.getConnection();
} catch (SQLException e) {
throw new DataQueryException("获取数据库连接出错");
}
}
@Override
public boolean valid() {
Connection conn = null;
try {
conn = dataSource.getConnection();
return conn.isValid(0);
} catch (SQLException e) {
throw new DataQueryException("检测连通性出错");
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
throw new DataQueryException("关闭数据库连接出错");
}
}
}
}
@Override
public void close() {
if (dataSource instanceof HikariDataSource) {
((HikariDataSource) dataSource).close();
} else {
throw new DataQueryException("不合法数据源类型");
}
}
@Override
public List<DbColumn> getTableColumns(String dbName, String tableName) {
String sql = dbDialect.columns(dbName, tableName);
if (dbDialect instanceof OracleDialect) {
List<DbColumn> longColumns = jdbcTemplate.query(sql, dbDialect.columnLongMapper());
List<DbColumn> queryColumns = jdbcTemplate.query(sql, dbDialect.columnMapper());
for (int i = 0; i < longColumns.size(); i++) {
DbColumn longColumn = longColumns.get(i);
DbColumn otherColumn = queryColumns.get(i);
otherColumn.setDataDefault(longColumn.getDataDefault());
}
return queryColumns;
}
return jdbcTemplate.query(sql, dbDialect.columnMapper());
}
@Override
public List<DbTable> getTables(String dbName) {
String sql = dbDialect.tables(dbName);
return jdbcTemplate.query(sql, dbDialect.tableMapper());
}
@Override
public DbTable getTable(String dbName, String tableName) {
String sql = dbDialect.tables(dbName);
List<DbTable> dbTables = jdbcTemplate.query(sql, dbDialect.tableMapper());
if (null != dbTables && dbTables.size() > 0) {
return dbTables.get(0);
}
return null;
}
@Override
public List<String> getViews(String dbName) {
String sql = dbDialect.views(dbName);
return jdbcTemplate.query(sql, dbDialect.viewMapper());
}
@Override
public List<String> getSchemas() {
List<String> schemaNames = new ArrayList<>();
try {
DatabaseMetaData metaData = getConnection().getMetaData();
ResultSet schemas = metaData.getSchemas();
while (schemas.next()) {
String tableSchema = schemas.getString("TABLE_SCHEM");
if (null != tableSchema && !"".equals(tableSchema)) {
schemaNames.add(tableSchema);
}
}
} catch (SQLException e) {
log.error("获取schema失败:", e);
throw new DataQueryException("获取schema失败");
}
return schemaNames;
}
@Override
public int count(String sql) {
return jdbcTemplate.queryForObject(dbDialect.count(sql), Integer.class);
}
@Override
public int count(String sql, Object[] args) {
return jdbcTemplate.queryForObject(dbDialect.count(sql), args, Integer.class);
}
@Override
public int count(String sql, Map<String, Object> params) {
NamedParameterJdbcTemplate namedJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate);
return namedJdbcTemplate.queryForObject(dbDialect.count(sql), params, Integer.class);
}
@Override
public List<Map<String, Object>> queryList(String sql) {
return jdbcTemplate.queryForList(sql);
}
@Override
public List<Map<String, Object>> queryList(String sql, Object[] args) {
return jdbcTemplate.queryForList(sql, args);
}
@Override
public PageResult<Map<String, Object>> queryByPage(String sql, long offset, long size) {
int total = count(sql);
String pageSql = dbDialect.buildPaginationSql(sql, offset, size);
List<Map<String, Object>> records = jdbcTemplate.queryForList(pageSql);
return new PageResult<>(total, records);
}
@Override
public PageResult<Map<String, Object>> queryByPage(String sql, Object[] args, long offset, long size) {
int total = count(sql, args);
String pageSql = dbDialect.buildPaginationSql(sql, offset, size);
List<Map<String, Object>> records = jdbcTemplate.queryForList(pageSql, args);
return new PageResult<>(total, records);
}
@Override
public PageResult<Map<String, Object>> queryByPage(String sql, Map<String, Object> params, long offset, long size) {
int total = count(sql, params);
String pageSql = dbDialect.buildPaginationSql(sql, offset, size);
NamedParameterJdbcTemplate namedJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate);
List<Map<String, Object>> records = namedJdbcTemplate.queryForList(pageSql, params);
return new PageResult<>(total, records);
}
}

View File

@@ -0,0 +1,127 @@
package com.codvision.db.query;
import com.codvision.db.cache.DefaultSqlCache;
import com.codvision.db.core.DbColumn;
import com.codvision.db.core.DbTable;
import com.codvision.db.core.PageResult;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class CacheDbQueryFactoryBean extends AbstractDbQueryFactory {
/**
* 默认缓存5分钟
*/
private static long DEFAULT_EXPIRE = 5 * 60 * 1000;
private static DefaultSqlCache sqlCache = new DefaultSqlCache(100, DEFAULT_EXPIRE);
private <T> T putCacheValue(String key, T value, long ttl) {
sqlCache.put(key, value, ttl);
return value;
}
@Override
public List<DbColumn> getTableColumns(String dbName, String tableName) {
Object[] args = new Object[]{dbName, tableName};
Optional.ofNullable(sqlCache.get(sqlCache.buildSqlCacheKey(super.dataSource.toString() + ":getTableColumns", args)));
return super.getTableColumns(dbName, tableName);
}
@Override
public List<DbTable> getTables(String dbName) {
Object[] args = new Object[]{dbName};
String cacheKey = sqlCache.buildSqlCacheKey(super.dataSource.toString() + ":getTables", args);
return (List<DbTable>) Optional.ofNullable(sqlCache.get(cacheKey))
.orElse(putCacheValue(cacheKey, super.getTables(dbName), DEFAULT_EXPIRE));
}
@Override
public DbTable getTable(String dbName, String tableName) {
Object[] args = new Object[]{dbName};
String cacheKey = sqlCache.buildSqlCacheKey(super.dataSource.toString() + ":getTable", args);
return (DbTable) Optional.ofNullable(sqlCache.get(cacheKey))
.orElse(putCacheValue(cacheKey, super.getTable(dbName, tableName), DEFAULT_EXPIRE));
}
@Override
public List<String> getViews(String dbName) {
Object[] args = new Object[]{dbName};
String cacheKey = sqlCache.buildSqlCacheKey(super.dataSource.toString() + ":getViews", args);
return (List<String>) Optional.ofNullable(sqlCache.get(cacheKey))
.orElse(putCacheValue(cacheKey, super.getViews(dbName), DEFAULT_EXPIRE));
}
@Override
public List<String> getSchemas() {
return super.getSchemas();
}
@Override
public int count(String sql) {
String cacheKey = sqlCache.buildSqlCacheKey(super.dataSource.toString() + ":" + sql, null);
return (int) Optional.ofNullable(sqlCache.get(cacheKey))
.orElse(putCacheValue(cacheKey, super.count(sql), DEFAULT_EXPIRE));
}
@Override
public int count(String sql, Object[] args) {
String cacheKey = sqlCache.buildSqlCacheKey(super.dataSource.toString() + ":" + sql, args);
return (int) Optional.ofNullable(sqlCache.get(cacheKey))
.orElse(putCacheValue(cacheKey, super.count(sql, args), DEFAULT_EXPIRE));
}
@Override
public int count(String sql, Map<String, Object> params) {
String cacheKey = sqlCache.buildSqlCacheKey(super.dataSource.toString() + ":" + sql, params.values().toArray());
return (int) Optional.ofNullable(sqlCache.get(cacheKey))
.orElse(putCacheValue(cacheKey, super.count(sql, params), DEFAULT_EXPIRE));
}
@Override
public List<Map<String, Object>> queryList(String sql) {
String cacheKey = sqlCache.buildSqlCacheKey(super.dataSource.toString() + ":" + sql, null);
return (List<Map<String, Object>>) Optional.ofNullable(sqlCache.get(cacheKey))
.orElse(putCacheValue(cacheKey, super.queryList(sql), DEFAULT_EXPIRE));
}
@Override
public List<Map<String, Object>> queryList(String sql, Object[] args) {
String cacheKey = sqlCache.buildSqlCacheKey(super.dataSource.toString() + ":" + sql, args);
return (List<Map<String, Object>>) Optional.ofNullable(sqlCache.get(cacheKey))
.orElse(putCacheValue(cacheKey, super.queryList(sql, args), DEFAULT_EXPIRE));
}
@Override
public PageResult<Map<String, Object>> queryByPage(String sql, long offset, long size) {
Object[] args = new Object[]{offset, size};
String cacheKey = sqlCache.buildSqlCacheKey(super.dataSource.toString() + ":" + sql, args);
return (PageResult<Map<String, Object>>) Optional.ofNullable(sqlCache.get(cacheKey))
.orElse(putCacheValue(cacheKey, super.queryByPage(sql, offset, size), DEFAULT_EXPIRE));
}
@Override
public PageResult<Map<String, Object>> queryByPage(String sql, Object[] args, long offset, long size) {
Object[] objects = Arrays.copyOf(args, args.length + 2);
objects[args.length] = offset;
objects[args.length + 1] = size;
String cacheKey = sqlCache.buildSqlCacheKey(super.dataSource.toString() + ":" + sql, objects);
return (PageResult<Map<String, Object>>) Optional.ofNullable(sqlCache.get(cacheKey))
.orElse(putCacheValue(cacheKey, super.queryByPage(sql, args, offset, size), DEFAULT_EXPIRE));
}
@Override
public PageResult<Map<String, Object>> queryByPage(String sql, Map<String, Object> params, long offset, long size) {
Object[] args = params.values().toArray();
Object[] objects = Arrays.copyOf(args, args.length + 2);
objects[args.length] = offset;
objects[args.length + 1] = size;
String cacheKey = sqlCache.buildSqlCacheKey(super.dataSource.toString() + ":" + sql, objects);
return (PageResult<Map<String, Object>>) Optional.ofNullable(sqlCache.get(cacheKey))
.orElse(putCacheValue(cacheKey, super.queryByPage(sql, params, offset, size), DEFAULT_EXPIRE));
}
}

View File

@@ -0,0 +1,5 @@
package com.codvision.db.query;
public class DefaultDbQueryFactoryBean extends AbstractDbQueryFactory {
}

View File

@@ -0,0 +1,95 @@
package com.codvision.db.utils;
import cn.hutool.core.util.StrUtil;
import com.codvision.db.enums.DbTypeEnum;
/**
* 数据库工具
*
* @author lingee
* @date 2023/10/12
*/
public class DbUtil {
public static DbTypeEnum getDatabaseType(String url) {
int startIndex = url.indexOf(":") + 1;
int endIndex = url.indexOf("://");
String type = url.substring(startIndex, endIndex).toLowerCase();
return DbTypeEnum.resolve(type);
}
/**
* 获取数据库连接中的数据库
*
* @param url 数据库连接
* @return 数据库
*/
public static String getDatabase(String url) {
String database = "";
if (StrUtil.isNotBlank(url)) {
int begin = url.lastIndexOf("/");
int end = url.lastIndexOf("?");
if (begin > -1 && end > -1 && end > begin) {
database = url.substring(begin + 1, end);
} else {
if (begin > -1) {
database = url.substring(begin + 1);
}
}
}
return database;
}
/**
* 获取数据连接中的端口
*
* @param url 数据库连接
* @return 端口号
*/
public static String getPort(String url) {
String port = "";
if (StrUtil.isNotBlank(url)) {
int begin = url.indexOf("//");
int end = url.lastIndexOf("/");
if (begin > -1 && end > -1 && end > begin) {
String hostStr = url.substring(begin + 2, end + 1);
int index = hostStr.indexOf(":");
int indexE = hostStr.indexOf("/");
if (index > 0) {
port = hostStr.substring(index + 1, indexE);
} else {
port = hostStr;
}
}
}
return port;
}
/**
* 获取数据库连接中的主机地址
*
* @param url 数据库连接
* @return 主机地址
*/
public static String getHost(String url) {
String host = "";
if (StrUtil.isNotBlank(url)) {
int begin = url.indexOf("//");
int end = url.lastIndexOf("/");
if (begin > -1 && end > -1 && end > begin) {
String hostStr = url.substring(begin + 2, end);
int index = hostStr.indexOf(":");
if (index > 0) {
host = hostStr.substring(0, index);
} else {
host = hostStr;
}
}
}
return host;
}
}

View File

@@ -0,0 +1,173 @@
package com.codvision.db.utils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.dbcp.BasicDataSource;
import org.apache.commons.dbcp.BasicDataSourceFactory;
import org.apache.commons.dbutils.QueryRunner;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import java.util.Properties;
/**
* JDBC操作工具类
*
* @author zhaochun
*/
@Slf4j
@SuppressWarnings("unused")
public class JdbcUtil {
/**
* 数据源,即连接池
*/
private final DataSource dataSource;
/**
* SQL执行器
*/
private final QueryRunner sqlRunner;
/**
* JdbcUtil构造方法
*
* @param driverClassName JDBC驱动包
* @param url JDBC连接
* @param username JDBC连接用户
* @param password JDBC连接用户密码
*/
public JdbcUtil(String driverClassName, String url, String username, String password) {
Properties properties = new Properties();
properties.setProperty("driverClassName", driverClassName);
properties.setProperty("url", url);
properties.setProperty("username", username);
properties.setProperty("password", password);
properties.setProperty("defaultTransactionIsolation", "READ_COMMITTED");
properties.setProperty("maxActive", "5");
properties.setProperty("maxIdle", "5");
properties.setProperty("minIdle", "0");
properties.setProperty("maxWait", "0");
properties.setProperty("initialSize", "1");
log.debug("DataSource Properties : {}", properties);
try {
this.dataSource = BasicDataSourceFactory.createDataSource(properties);
this.sqlRunner = new QueryRunner(this.dataSource);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 关闭连接池
*/
public void close() {
if (this.dataSource instanceof BasicDataSource) {
try {
log.info("dataSource close...");
((BasicDataSource) this.dataSource).close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
/**
* 获取连接池
*
* @return 数据源
*/
public DataSource getDataSource() {
return dataSource;
}
/**
* 从连接池获取JDBC连接
*
* @return JDBC连接
* @throws SQLException SQL异常
*/
public Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
/**
* 获取SQL执行器
*
* @return 查询执行器
*/
public QueryRunner getRunner() {
return sqlRunner;
}
/**
* 执行SQL(自动从连接池获取JDBC连接结束后自动关闭)
*
* @param sql sql文
* @param params sql参数
*/
public void execute(String sql, Object... params) {
log.debug("execute(String sql, Object... params), sql:[{}], params:{}", sql, params);
try {
sqlRunner.execute(sql, params);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 执行SQL(使用传入的JDBC连接结束后不关闭)
*
* @param connection JDBC连接
* @param sql sql文
* @param params sql参数
*/
public void execute(Connection connection, String sql, Object... params) {
log.debug("execute(Connection connection, String sql, Object... params), sql:[{}], params:{}", sql, params);
try {
sqlRunner.execute(connection, sql, params);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 开启一个新事务执行多条SQL语句
*
* <p>由于DDL会即时提交事务此处传入的SQL语句应都是DML语句。</p>
*
* @param sqls SQL语句集合
*/
public void executeWithTranslation(List<String> sqls) {
try (Connection connection = dataSource.getConnection()) {
connection.setAutoCommit(false);
log.debug("executeWithTranslation start.");
for (String sql : sqls) {
execute(connection, sql);
}
connection.commit();
log.debug("executeWithTranslation commit.");
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 在传入的JDBC连接中执行SQL语句(不主动提交事务)
*
* <p>不主动提交事务的含义:</p>
* <p>如果传入的connection的autocommit为true此处当然也会自动提交</p>
* <p>如果传入的connection的autocommit为false此处自然不会主动提交。</p>
*
* <p>由于DDL会即时提交事务此处传入的SQL语句应都是DML语句。</p>
*
* @param connection JDBC连接
* @param sqls SQL语句集合
*/
public void executeWithConnection(Connection connection, List<String> sqls) {
for (String sql : sqls) {
execute(connection, sql);
}
}
}

View File

@@ -0,0 +1,106 @@
package com.codvision.db.utils;
import cn.hutool.core.util.StrUtil;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* SQL脚本阅读器
*
* @author zhaochun
*/
public class ScriptReader {
/**
* 脚本文件输入流
*
* <p>此处设计为InputStream的原因</p>
* <p>要考虑sql脚本直接打包在jar包的resource目录下的场景jar中的文件不能直接作为FileSystem的文件访问因此不能直接设计为文件路径。</p>
*/
private final InputStream inputStream;
/**
* 脚本字符集
*
* <p>默认SQL脚本的字符集为UTF-8目前不支持其他字符集。</p>
*/
private Charset charset = StandardCharsets.UTF_8;
/**
* ScriptReader构造方法
*
* @param inputStream 脚本文件输入流
*/
public ScriptReader(InputStream inputStream) {
this.inputStream = inputStream;
}
/**
* ScriptReader构造方法
*
* @param inputStream 脚本文件输入流
* @param charset 脚本文件字符集
*/
@SuppressWarnings("unused")
public ScriptReader(InputStream inputStream, Charset charset) {
this.inputStream = inputStream;
this.charset = charset;
}
/**
* 读取SQL脚本文件
*
* @return SQL语句集合
*/
public List<String> readSqls() {
List<String> sqls = new ArrayList<>();
String[] containStrs = {"$BODY$", "$do$"};
boolean containFlag = false;
try (BufferedReader br = new BufferedReader(new InputStreamReader(this.inputStream, charset))) {
// 生成sql构造器
StringBuilder sqlBuilder = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
// 去除首位空白字符
String lineStrip = StrUtil.trim(line);
// 去除空行、注释行
if (StrUtil.isBlank(lineStrip) || lineStrip.startsWith("--")) {
continue;
}
for (String containStr : containStrs) {
if (lineStrip.contains(containStr)) {
containFlag = !containFlag;
break;
}
}
if (lineStrip.endsWith(";") && !containFlag) {
// 如果该行以";"结尾则认为该条sql语句结束
// 先去除末尾分号
String tmpLine = lineStrip.substring(0, lineStrip.length() - 1);
// 将该行加入sql构造器
sqlBuilder.append(tmpLine).append(" \n");
// 将sql构造器转为sql语句加入sql语句集合
sqls.add(StrUtil.trim(sqlBuilder.toString()));
// 清空sql构造器
sqlBuilder = new StringBuilder();
} else {
// 如果该行没有以";"结尾则认为该条sql语句尚未结束
sqlBuilder.append(lineStrip).append(" \n");
}
}
// 特殊场景处理如果sql脚本最后一条sql语句没有写";"结尾则需要将非空的sql构造器转为sql语句并加入sql语句集合。
if (sqlBuilder.length() > 0) {
sqls.add(StrUtil.trim(sqlBuilder.toString()));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return sqls;
}
}

View File

@@ -0,0 +1,170 @@
package com.codvision.db.versionctl;
import cn.hutool.core.util.StrUtil;
import com.codvision.db.utils.JdbcUtil;
import com.codvision.db.versionctl.chain.DbVersionCtlContext;
import com.codvision.db.versionctl.task.*;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* 数据库版本控制器
*
* @author lingee
*/
@SuppressWarnings("unused")
public class DbVersionCtl {
/**
* 数据库版本控制配置属性集
*/
private final DbVersionCtlProps dbVersionCtlProps;
/**
* DbVersionCtl构造方法
*
* @param dbVersionCtlProps 数据库版本控制配置属性集
*/
public DbVersionCtl(DbVersionCtlProps dbVersionCtlProps) {
this.dbVersionCtlProps = dbVersionCtlProps;
}
/**
* 控制数据库版本升级
*
* <p>1. 准备JDBC操作工具和数据库版本控制上下文对象</p>
* <p>2. 判断本次数据库版本控制的操作模式</p>
* <p>3. 根据操作模式组装任务链</p>
* <p>4. 启动任务链</p>
* <p>5. 关闭JDBC连接与连接池</p>
*/
public void doDBVersionControl() {
// 准备JDBC操作工具
JdbcUtil jdbcUtil = new JdbcUtil(dbVersionCtlProps.getDriverClassName(),
this.dbVersionCtlProps.getUrl(),
this.dbVersionCtlProps.getUsername(),
this.dbVersionCtlProps.getPassword());
DbVersionCtlContext context = null;
try {
// 准备上下文对象
context = new DbVersionCtlContext(
this.dbVersionCtlProps,
jdbcUtil);
// 判断本次数据库版本控制的操作模式
OperationMode operationMode = chargeOperationMode(jdbcUtil);
// 根据操作模式组装任务链
assemblyTaskChain(context, operationMode);
// 启动任务链
context.pollTask().doMyWork();
} finally {
// 关闭JDBC连接与连接池
Optional.ofNullable(context).ifPresent(DbVersionCtlContext::closeGcJdbcUtil);
}
}
private void assemblyTaskChain(DbVersionCtlContext context, OperationMode operationMode) {
switch (operationMode) {
case DEPLOY_INIT:
context.offerTask(new CreateVersionTblTask(context));
break;
case BASELINE_INIT:
context.offerTask(new CreateVersionTblTask(context));
context.offerTask(new InsertBaselineTask(context));
break;
case BASELINE_RESET:
context.offerTask(new DropVersionTblTask(context));
context.offerTask(new CreateVersionTblTask(context));
context.offerTask(new InsertBaselineTask(context));
break;
case DEPLOY_INCREASE:
if ("y".equalsIgnoreCase(dbVersionCtlProps.getModifyDbVersionTable())) {
context.offerTask(new ModifyVersionTblTask(context));
}
break;
}
context.offerTask(new IncreaseVersionTask(context));
}
private OperationMode chargeOperationMode(JdbcUtil jdbcUtil) {
// 判断当前database是否非空
List<String> tblNames = queryExistTblNames(jdbcUtil);
if (tblNames.isEmpty()) {
// 当前database为空首次启动服务导入全部数据库脚本并创建数据库版本控制表并生成数据库版本记录。
return OperationMode.DEPLOY_INIT;
} else {
// 如果当前database非空判断是否已经创建了数据库版本控制表"cvbp_db_version_ctl"
final String dbVersionTableName = this.dbVersionCtlProps.getDbVersionTableName();
if (tblNames.stream().anyMatch(dbVersionTableName::equals)) {
// 判断是否需要重置数据库版本控制表
if ("y".equals(this.dbVersionCtlProps.getBaselineReset())
&& StrUtil.isNotBlank(this.dbVersionCtlProps.getBaselineResetConditionSql())
&& checkBaselineResetConditionSql(jdbcUtil)) {
// 查询数据库版本控制表的最新记录。
// 只有属性[baselineResetConditionSql]配置的sql查询到有记录才会执行基线重置操作。
// baselineResetConditionSql在配置时建议将install_time字段作为条件去查询这样以后不会再有满足该条件的记录。
return OperationMode.BASELINE_RESET;
}
// 已经存在数据库版本控制表根据当前资源目录下的sql脚本与版本控制表中各个业务空间的最新版本做增量的sql脚本执行。
return OperationMode.DEPLOY_INCREASE;
} else {
// database非空但还没有数据库版本控制表根据配置参数[baselineBusinessSpaceAndVersions]决定各个业务空间的基线版本,
// 创建数据库版本控制表生成baseline记录然后做增量的sql脚本执行。
return OperationMode.BASELINE_INIT;
}
}
}
private boolean checkBaselineResetConditionSql(JdbcUtil jdbcUtil) {
try {
return jdbcUtil.getRunner().query(this.dbVersionCtlProps.getBaselineResetConditionSql(), ResultSet::next);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private List<String> queryExistTblNames(JdbcUtil jdbcUtil) {
try {
List<String> tableNames = new ArrayList<>();
jdbcUtil.getRunner().execute(
this.dbVersionCtlProps.getExistTblQuerySql(),
resultSet -> {
while (resultSet.next()) {
tableNames.add(resultSet.getString(1));
}
return null;
});
return tableNames;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public enum OperationMode {
// 项目首次部署,数据库没有任何表。
// 该操作会生成数据库版本控制表,执行数据库初始化脚本,更新数据库版本控制表数据。
DEPLOY_INIT,
// 项目增量部署,之前已经导入业务表与数据库版本控制表。
// 该操作根据已有的数据库版本控制表中的记录判断哪些脚本需要执行,然后执行脚本并插入新的数据库版本记录。
DEPLOY_INCREASE,
// 一个已经上线的项目初次使用数据库版本控制,之前已经导入业务表,但没有数据库版本控制表。
// 该操作会创建数据库版本控制表,并写入一条版本基线记录,然后基于属性配置的基线版本确定哪些脚本需要执行。
// 执行脚本后向数据库版本控制表插入新的版本记录。
BASELINE_INIT,
// 对一个已经使用数据库版本控制的项目,重置其数据库版本的基线。
// 该操作会删除既有的数据库版本控制表,然后重新做一次`BASELINE_INIT`操作。
// 注意该操作需要特殊的属性控制,要慎用。
BASELINE_RESET
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright (c) 2023 codvision.com All Rights Reserved.
*/
package com.codvision.db.versionctl;
import cn.hutool.core.util.StrUtil;
import com.codvision.commoncore.exception.BusinessException;
import com.codvision.db.config.DBVCTLProps;
import com.codvision.db.utils.DbUtil;
import org.springframework.core.env.Environment;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
/**
* 数据库版本控制器
*
* @author lingee
* @date 2023/12/5
*/
public class DbVersionCtlInitializer {
@Resource
private DBVCTLProps dbvctlProps;
@Resource
protected Environment env;
@PostConstruct
public void init() {
// 开启自动控制时才会自动执行数据库版本升级
if (dbvctlProps.getEnabled()) {
dbVersionCtl();
}
}
/**
* 用于手动执行数据库版本升级
*/
public void dbVersionCtlManual() {
if (!dbvctlProps.getEnabled()) {
dbVersionCtl();
}
}
private void dbVersionCtl() {
// 如果已经配置了spring的数据源则可以直接使用它们而不必重复配置JDBC连接属性
String jdbcClass = env.getProperty("spring.datasource.driver-class-name");
String jdbcUrl = env.getProperty("spring.datasource.url");
String jdbcUser = env.getProperty("spring.datasource.username");
String jdbcPwd = env.getProperty("spring.datasource.password");
if (StrUtil.isBlank(jdbcUrl)) {
throw new BusinessException("未配置数据库连接");
}
DbVersionCtlProps dbVersionCtlProps = new DbVersionCtlProps();
dbVersionCtlProps.setBaselineBusinessSpaceAndVersions(dbvctlProps.getBaselineBusinessSpaceAndVersions());
dbVersionCtlProps.setDriverClassName(jdbcClass);
dbVersionCtlProps.setUrl(jdbcUrl);
dbVersionCtlProps.setUsername(jdbcUser);
dbVersionCtlProps.setPassword(jdbcPwd);
String existTblQuerySql = "";
switch (DbUtil.getDatabaseType(jdbcUrl)) {
case mysql:
existTblQuerySql = "show tables";
break;
case postgresql:
existTblQuerySql = "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname NOT IN ('pg_catalog', 'information_schema')";
break;
}
dbVersionCtlProps.setExistTblQuerySql(existTblQuerySql);
dbVersionCtlProps.setBaselineReset(dbvctlProps.getBaselineReset() ? "y" : "n");
dbVersionCtlProps.setBaselineResetConditionSql(dbvctlProps.getBaselineResetConditionSql());
DbVersionCtl dbVersionCtl = new DbVersionCtl(dbVersionCtlProps);
dbVersionCtl.doDBVersionControl();
}
}

View File

@@ -0,0 +1,268 @@
package com.codvision.db.versionctl;
import cn.hutool.core.util.StrUtil;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 数据库版本控制配置属性集
*
* @author lingee
*/
@SuppressWarnings("unused")
public class DbVersionCtlProps {
/**
* sql脚本资源类型classpath/filesystem默认classpath
*/
private ScriptResourceMode scriptResourceMode = ScriptResourceMode.CLASSPATH;
/**
* sql脚本文件目录多个时用","连接。例如:"classpath:db/raven/,classpath:db/sentry/"
*/
private String scriptDirs = "classpath*:db/migration/";
/**
* 数据库非空但首次使用数据库版本管理时,指定生成版本基线的业务空间及其基线版本,多个业务空间时使用逗号连接。
* 例如:"raven_V1.0.0,sentry_V1.1.2"
*/
private String baselineBusinessSpaceAndVersions;
/**
* 数据库版本管理表,默认"cvbp_db_version_ctl"
*/
private String dbVersionTableName = "cvbp_db_version_ctl";
/**
* 数据库版本管理表建表文路径,默认"classpath:db/versionctl/create_cvbp_db_version_ctl.sql"
*/
private String dbVersionTableCreateSqlPath = "classpath:db/versionctl/create_cvbp_db_version_ctl.sql";
/**
* JDBC驱动类
*/
private String driverClassName;
/**
* JDBC连接URL
*/
private String url;
/**
* JDBC连接用户
*/
private String username;
/**
* JDBC连接用户密码
*/
private String password;
/**
* 查看当前database所有表的sql默认pgsql
*/
private String existTblQuerySql;
/**
* 是否重置数据库基线版本
*/
private String baselineReset = "n";
/**
* 数据库基线版本重置条件SQL只有[baselineReset]设置为"y"且该SQL查询结果非空才会进行数据库基线版本重置操作
* 通常建议使用时间戳字段[install_time]作为查询SQL的条件这样只会生效一次
* 以后升级版本时即使忘记将【baselineReset】属性清除或设置为"n"也不会导致数据库基线版本被误重置。
*/
private String baselineResetConditionSql = "";
/**
* 是否修改DbVersionTable结构
*/
private String modifyDbVersionTable = "n";
/**
* 修改DbVersionTable的SQL
*/
private String modifyDbVersionTableSqlPath = "classpath:db/versionctl/modify_cvbp_db_version_ctl.sql";
public ScriptResourceMode getScriptResourceMode() {
return scriptResourceMode;
}
public void setScriptResourceMode(ScriptResourceMode scriptResourceMode) {
if (scriptResourceMode != null) {
this.scriptResourceMode = scriptResourceMode;
}
}
public String getScriptDirs() {
checkScriptDirs(this.scriptDirs);
return scriptDirs;
}
public void setScriptDirs(String scriptDirs) {
checkScriptDirs(scriptDirs);
this.scriptDirs = StrUtil.trim(scriptDirs);
}
public String getBaselineBusinessSpaceAndVersions() {
return baselineBusinessSpaceAndVersions;
}
public void setBaselineBusinessSpaceAndVersions(String baselineBusinessSpaceAndVersions) {
this.baselineBusinessSpaceAndVersions = baselineBusinessSpaceAndVersions;
}
public String getDbVersionTableName() {
return dbVersionTableName;
}
public void setDbVersionTableName(String dbVersionTableName) {
if (StrUtil.isNotBlank(dbVersionTableName)) {
this.dbVersionTableName = dbVersionTableName;
}
}
public String getDbVersionTableCreateSqlPath() {
return dbVersionTableCreateSqlPath;
}
public void setDbVersionTableCreateSqlPath(String dbVersionTableCreateSqlPath) {
if (StrUtil.isNotBlank(dbVersionTableCreateSqlPath)) {
this.dbVersionTableCreateSqlPath = dbVersionTableCreateSqlPath;
}
}
public String getDriverClassName() {
return driverClassName;
}
public void setDriverClassName(String driverClassName) {
this.driverClassName = driverClassName;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getExistTblQuerySql() {
return existTblQuerySql;
}
public void setExistTblQuerySql(String existTblQuerySql) {
if (StrUtil.isNotBlank(existTblQuerySql)) {
this.existTblQuerySql = existTblQuerySql;
}
}
public String getBaselineReset() {
return baselineReset;
}
public void setBaselineReset(String baselineReset) {
if ("y".equalsIgnoreCase(baselineReset)) {
this.baselineReset = "y";
}
}
public String getBaselineResetConditionSql() {
return baselineResetConditionSql;
}
public void setBaselineResetConditionSql(String baselineResetConditionSql) {
if (StrUtil.isNotBlank(baselineResetConditionSql)) {
this.baselineResetConditionSql = StrUtil.trim(baselineResetConditionSql);
}
}
public String getModifyDbVersionTable() {
return modifyDbVersionTable;
}
public void setModifyDbVersionTable(String modifyDbVersionTable) {
if ("y".equalsIgnoreCase(modifyDbVersionTable)) {
this.modifyDbVersionTable = "y";
}
}
public String getModifyDbVersionTableSqlPath() {
return modifyDbVersionTableSqlPath;
}
public void setModifyDbVersionTableSqlPath(String modifyDbVersionTableSqlPath) {
if (StrUtil.isNotBlank(modifyDbVersionTableSqlPath)) {
this.modifyDbVersionTableSqlPath = modifyDbVersionTableSqlPath;
}
}
/**
* 检查并获取SQL脚本目录集合
*
* @return SQL脚本目录集合
*/
public List<String> makeScriptDirPaths() {
checkScriptDirs(this.scriptDirs);
return Arrays.stream(this.scriptDirs.split(","))
.map(StrUtil::trim)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toList());
}
private void checkScriptDirs(String scriptDirs) {
if (StrUtil.isBlank(scriptDirs)) {
throw new RuntimeException("DbVersionCtlProps.scriptDirs is empty!");
}
}
/**
* SQL脚本资源模式
*/
public enum ScriptResourceMode {
/**
* 采用"classpath:xxx/xxx/"的方式配置SQL脚本目录一般将SQL脚本置于java工程的resource目录下并打包到jar中。
*/
CLASSPATH,
/**
* 采用"/xx/xx/"文件系统目录的方式配置SQL脚本目录一般将SQL脚本置于操作系统的文件目录下通常是绝对目录。
*/
FILESYSTEM;
public static ScriptResourceMode getScriptResourceMode(String mode) {
if (StrUtil.isBlank(mode)) {
throw new RuntimeException("DbVersionCtlProps.mode can not be empty!");
}
String modeAfterLower = StrUtil.trim(mode).toLowerCase();
if ("classpath".equals(modeAfterLower)) {
return CLASSPATH;
} else if ("filesystem".equals(modeAfterLower)) {
return FILESYSTEM;
} else {
throw new RuntimeException("DbVersionCtlProps.mode should be classpath or filesystem! Not " + mode + " !");
}
}
}
}

View File

@@ -0,0 +1,132 @@
package com.codvision.db.versionctl.chain;
import com.codvision.db.utils.JdbcUtil;
import com.codvision.db.utils.ScriptReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import java.io.*;
import java.sql.Connection;
import java.util.List;
import java.util.stream.Collectors;
/**
* 数据库版本控制任务抽象类
*
* @author lingee
*/
@SuppressWarnings("unused")
public abstract class DbVersionCtlAbstractTask implements DbVersionCtlTask {
protected Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 数据库版本控制上下文
*/
protected final DbVersionCtlContext context;
/**
* JDBC操作工具
*/
protected final JdbcUtil jdbcUtil;
/**
* SQL脚本执行用JDBC连接
*/
protected final Connection connection;
/**
* DbVersionCtlAbstractTask构造方法
*
* @param context 数据库版本控制上下文
*/
public DbVersionCtlAbstractTask(DbVersionCtlContext context) {
this.context = context;
this.jdbcUtil = context.getJdbcUtil();
this.connection = context.getConnection();
}
/**
* 执行任务并返回成功与否结果
*
* @return 任务执行成功与否
*/
public abstract boolean runTask();
/**
* 实现DbVersionCtlTask接口方法doMyWork
*
* <p>本任务执行成功时再调用下一个任务。</p>
*
* @see DbVersionCtlTask
*/
@Override
public void doMyWork() {
if (runTask()) {
callNext();
}
}
/**
* 实现DbVersionCtlTask接口方法callNext
*
* <p>从上下文获取下一个任务,非空则执行。</p>
*
* @see DbVersionCtlTask
*/
@Override
public void callNext() {
DbVersionCtlTask nextTask = this.context.pollTask();
if (nextTask != null) {
nextTask.doMyWork();
}
}
@SuppressWarnings("squid:S112")
protected InputStream loadInputStreamFromClassPath(String classPath) {
InputStream inputStream;
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
try {
Resource[] resources = resolver.getResources(classPath);
if (resources.length == 0) {
inputStream = null;
} else {
inputStream = resources[0].getInputStream();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return inputStream;
}
@SuppressWarnings("squid:S112")
protected InputStream loadInputStreamFromFile(String filePath) {
InputStream inputStream;
File sqlFile = new File(filePath);
if (sqlFile.exists() && sqlFile.isFile()) {
try {
inputStream = new FileInputStream(sqlFile);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
} else {
inputStream = null;
}
return inputStream;
}
protected List<String> readSqlFromInputStream(String dbVersionTableName, boolean needReplaceTblName, InputStream inputStream) {
ScriptReader scriptReader = new ScriptReader(inputStream);
List<String> sqlLines = scriptReader.readSqls();
if (needReplaceTblName) {
return sqlLines.stream()
.map(sqlLine -> sqlLine.replace("cvbp_db_version_ctl", dbVersionTableName))
.collect(Collectors.toList());
} else {
return sqlLines;
}
}
}

View File

@@ -0,0 +1,155 @@
package com.codvision.db.versionctl.chain;
import com.codvision.db.utils.JdbcUtil;
import com.codvision.db.versionctl.DbVersionCtlProps;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* 数据库版本控制上下文
*
* @author lingee
*/
@SuppressWarnings("unused")
public class DbVersionCtlContext {
/**
* 数据库版本控制表字段(不带ID)
*/
public static final String DB_VERSION_CTL_COLS_WITHOUT_ID = "business_space, major_version, minor_version, patch_version, extend_version, version, custom_name, version_type, script_file_name, script_digest_hex, success, execution_time, install_time, install_user";
/**
* 数据库版本控制表字段插入SQL文VALUES片段
*/
public static final String DB_VERSION_CTL_INSERT_VALUES = " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
/**
* 日期格式 "yyyy-MM-dd HH:mm:ss"
*/
public static final String DATETIME_PTN = "yyyy-MM-dd HH:mm:ss";
/**
* 数据库版本控制配置属性集
*/
private final DbVersionCtlProps dbVersionCtlProps;
/**
* JDBC操作工具
*/
private final JdbcUtil jdbcUtil;
/**
* JDBC连接
*/
private final Connection connection;
/**
* 数据库版本控制任务队列
*/
private final BlockingQueue<DbVersionCtlTask> tasks = new LinkedBlockingQueue<>();
/**
* DbVersionCtlContext构造方法
*
* @param dbVersionCtlProps 数据库版本控制配置属性集
* @param jdbcUtil JDBC操作工具
*/
public DbVersionCtlContext(DbVersionCtlProps dbVersionCtlProps, JdbcUtil jdbcUtil) {
this.dbVersionCtlProps = dbVersionCtlProps;
this.jdbcUtil = jdbcUtil;
try {
this.connection = jdbcUtil.getConnection();
this.connection.setAutoCommit(true);
} catch (SQLException e) {
jdbcUtil.close();
throw new RuntimeException(e);
}
}
public DbVersionCtlProps getDbVersionCtlProps() {
return dbVersionCtlProps;
}
public JdbcUtil getJdbcUtil() {
return jdbcUtil;
}
public Connection getConnection() {
return connection;
}
public BlockingQueue<DbVersionCtlTask> getTasks() {
return tasks;
}
/**
* 向任务队列添加一个任务。
*
* <p>offer在入队失败时返回false但此处没有做失败预期处理。</p>
*
* @param task 将要添加的任务
*/
public void offerTask(DbVersionCtlTask task) {
//noinspection ResultOfMethodCallIgnored
this.tasks.offer(task);
}
/**
* 从任务队列拉取下一个任务没有则返回null。
*
* @return 拉取的任务
*/
public DbVersionCtlTask pollTask() {
return this.tasks.poll();
}
/**
* 关闭上下文中的JDBC连接以及JDBC操作工具的连接池
*/
public void closeGcJdbcUtil() {
if (this.connection != null) {
try {
this.connection.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
this.jdbcUtil.close();
}
/**
* 生成数据库版本控制表插入SQL文
*
* @return 数据库版本控制表插入SQL文
*/
public String makeInsertSql() {
return "INSERT INTO " + this.dbVersionCtlProps.getDbVersionTableName()
+ "(" + DbVersionCtlContext.DB_VERSION_CTL_COLS_WITHOUT_ID + ") "
+ DbVersionCtlContext.DB_VERSION_CTL_INSERT_VALUES;
}
/**
* 生成数据库版本控制表更新SQL文
*
* @return 数据库版本控制表更新SQL文
*/
public String makeUpdateSql() {
return "UPDATE " + this.dbVersionCtlProps.getDbVersionTableName()
+ " set success = 1, execution_time = ? WHERE business_space = ? AND major_version = ? AND minor_version = ? AND patch_version = ? AND extend_version = ?";
}
/**
* 生成数据库版本控制表查询SQL文
*
* @return 数据库版本控制表查询SQL文
*/
public String makeSelectSql() {
return "SELECT id, " + DbVersionCtlContext.DB_VERSION_CTL_COLS_WITHOUT_ID
+ " FROM " + this.dbVersionCtlProps.getDbVersionTableName()
+ " WHERE business_space = ?"
+ " ORDER BY major_version DESC , minor_version DESC , patch_version DESC , extend_version DESC";
}
}

View File

@@ -0,0 +1,18 @@
package com.codvision.db.versionctl.chain;
/**
* 数据库版本控制任务接口
*
* @author lingee
*/
public interface DbVersionCtlTask {
/**
* 执行任务
*/
void doMyWork();
/**
* 调用下一个任务
*/
void callNext();
}

View File

@@ -0,0 +1,250 @@
package com.codvision.db.versionctl.entity;
/**
* 数据库版本控制表Entity
*
* @author lingee
*/
@SuppressWarnings("unused")
public class DbVersionEntity {
/**
* 版本记录ID
*
* <p>mysql中是一个自增ID作为该表物理主键其他数据库中可以不使用该字段。</p>
*/
private int id;
/**
* 业务空间
*
* <p>对应一个database下不同表的业务归属大部分场景直接使用database名即可。</p>
* <p>"业务空间+主版本号+次版本号+补丁版本号"作为逻辑主键。Mysql下即唯一性约束其他数据库可能是组合物理主键。</p>
*/
private String businessSpace;
/**
* 主版本号
*
* <p>"业务空间+主版本号+次版本号+补丁版本号"作为逻辑主键。Mysql下即唯一性约束其他数据库可能是组合物理主键。</p>
*/
private int majorVersion;
/**
* 次版本号
*
* <p>"业务空间+主版本号+次版本号+补丁版本号"作为逻辑主键。Mysql下即唯一性约束其他数据库可能是组合物理主键。</p>
*/
private int minorVersion;
/**
* 补丁版本号
*
* <p>"业务空间+主版本号+次版本号+补丁版本号"作为逻辑主键。Mysql下即唯一性约束其他数据库可能是组合物理主键。</p>
*/
private int patchVersion;
/**
* 扩展版本号
*
* <p>"业务空间+主版本号+次版本号+补丁版本号+扩展版本号"作为逻辑主键。Mysql下即唯一性约束其他数据库可能是组合物理主键。</p>
*/
private int extendVersion;
/**
* 版本号,按固定规律拼接而成:"V[主版本号].[次版本号].[补丁版本号].[扩展版本号]",如"V1.0.0.0"
*/
private String version;
/**
* 脚本自定义名称
*
* <p>即该版本执行的SQL脚本的自定义名称</p>
*/
private String customName;
/**
* 该版本记录类型支持两种SQL 或 BaseLine
*
* <p>SQL:由执行具体的某个SQL脚本而导致的数据库版本记录。</p>
* <p>BaseLine:对已经上线的项目做指定版本的基线版本记录。</p>
*/
private String versionType;
/**
* 脚本文件名
*
* <p>脚本文件名的命名约定:</p>
* <p>"[业务空间]_V[主版本号].[次版本号].[补丁版本号]_[脚本自定义名称].sql"</p>
* <p>如:"raven_V1.0.0_init.sql"</p>
*/
private String scriptFileName;
/**
* SQL脚本内容摘要(16进制)
*
* <p>目前该字段没有使用,是一个预留字段。</p>
* <p>预留该字段的目的是为了以后可能要检查已经执行过的SQL脚本的内容有没有发生变化。</p>
*/
private String scriptDigestHex;
/**
* SQL脚本执行结果1:已完成0未完成
*/
private byte success;
/**
* SQL脚本执行耗时单位毫秒
*/
private int executionTime;
/**
* SQL脚本开始执行时间
*/
private String installTime;
/**
* SQL脚本执行用户(JDBC连接用户)
*/
private String installUser;
@Override
public String toString() {
return "DbVersionEntity{" +
"id=" + id +
", businessSpace='" + businessSpace + '\'' +
", majorVersion=" + majorVersion +
", minorVersion=" + minorVersion +
", patchVersion=" + patchVersion +
", extendVersion=" + extendVersion +
", version='" + version + '\'' +
", customName='" + customName + '\'' +
", versionType=" + versionType +
", scriptFileName='" + scriptFileName + '\'' +
", scriptDigestHex='" + scriptDigestHex + '\'' +
", success=" + success +
", executionTime=" + executionTime +
", installTime='" + installTime + '\'' +
", installUser='" + installUser + '\'' +
'}';
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getBusinessSpace() {
return businessSpace;
}
public void setBusinessSpace(String businessSpace) {
this.businessSpace = businessSpace;
}
public int getMajorVersion() {
return majorVersion;
}
public void setMajorVersion(int majorVersion) {
this.majorVersion = majorVersion;
}
public int getMinorVersion() {
return minorVersion;
}
public void setMinorVersion(int minorVersion) {
this.minorVersion = minorVersion;
}
public int getPatchVersion() {
return patchVersion;
}
public void setPatchVersion(int patchVersion) {
this.patchVersion = patchVersion;
}
public int getExtendVersion() {
return extendVersion;
}
public void setExtendVersion(int extendVersion) {
this.extendVersion = extendVersion;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getCustomName() {
return customName;
}
public void setCustomName(String customName) {
this.customName = customName;
}
public String getVersionType() {
return versionType;
}
public void setVersionType(String versionType) {
this.versionType = versionType;
}
public String getScriptFileName() {
return scriptFileName;
}
public void setScriptFileName(String scriptFileName) {
this.scriptFileName = scriptFileName;
}
public String getScriptDigestHex() {
return scriptDigestHex;
}
public void setScriptDigestHex(String scriptDigestHex) {
this.scriptDigestHex = scriptDigestHex;
}
public byte getSuccess() {
return success;
}
public void setSuccess(byte success) {
this.success = success;
}
public int getExecutionTime() {
return executionTime;
}
public void setExecutionTime(int executionTime) {
this.executionTime = executionTime;
}
public String getInstallTime() {
return installTime;
}
public void setInstallTime(String installTime) {
this.installTime = installTime;
}
public String getInstallUser() {
return installUser;
}
public void setInstallUser(String installUser) {
this.installUser = installUser;
}
}

View File

@@ -0,0 +1,258 @@
package com.codvision.db.versionctl.entity;
import cn.hutool.core.io.IoUtil;
import cn.hutool.crypto.digest.DigestUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* SQL脚本Entity
*
* @author lingee
*/
@SuppressWarnings("unused")
public class SQLScriptEntity implements Comparable<SQLScriptEntity> {
// sql脚本文件名正则表达式
private static final Pattern PTN_SCRIPT_NAME_DEFAULT = Pattern.compile("^([A-Za-z0-9]+)_V(\\d+)\\.(\\d+)\\.(\\d+)_(\\w+)\\.sql$");
private static final Pattern PTN_SCRIPT_NAME_EXTEND = Pattern.compile("^([A-Za-z0-9]+)_V(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)_(\\w+)\\.sql$");
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/* SQLScript对象构建属性 */
// sql脚本文件名格式为"[业务空间]_V[major].[minor].[patch].[extend]_[自定义名称].sql"
private String fileName;
// 脚本输入流
private InputStream inputStream;
/* SQLScript对象常规属性 */
// 业务空间用于同一database下数据表的集合划分通常根据业务功能划分
// 同一个业务空间中的表的结构与初期数据的版本管理采用统一的版本号递增顺序;不同业务空间的版本号的递增顺序是不同的。
// 业务空间命名只支持大小写字母与数字
private String businessSpace;
// 主版本号,一个业务空间对应的主版本号,对应"x.y.z.t"中的x只支持非负整数
private int majorVersion;
// 次版本号,一个业务空间对应的次版本号,对应"x.y.z.t"中的y只支持非负整数
private int minorVersion;
// 补丁版本号,一个业务空间对应的补丁版本号,对应"x.y.z.t"中的z只支持非负整数
private int patchVersion;
// 扩展版本号,一个业务空间对应的扩展版本号,对应"x.y.z.t"中的4只支持非负整数
private int extendVersion;
// 一个业务空间的完整版本号,格式为"[businessSpace]_V[majorVersion].[minorVersion].[patchVersion]"
private String version;
// 该sql脚本的自定义名称支持大小写字母数字与下划线
private String customName;
// 该sql脚本对应的摘要
private String hex;
/**
* SQLScriptEntity构造方法
*
* @param fileName SQL脚本文件名
* @param inputStream SQL脚本输入流
*/
public SQLScriptEntity(String fileName, InputStream inputStream) {
this.fileName = fileName;
ByteArrayOutputStream os = new ByteArrayOutputStream();
IoUtil.copy(inputStream, os);
this.inputStream = IoUtil.toStream(os.toByteArray());
this.hex = DigestUtil.md5Hex(os.toByteArray());
Matcher matcherDefault = PTN_SCRIPT_NAME_DEFAULT.matcher(fileName);
if (matcherDefault.matches()) {
this.businessSpace = matcherDefault.group(1);
this.majorVersion = Integer.parseInt(matcherDefault.group(2));
this.minorVersion = Integer.parseInt(matcherDefault.group(3));
this.patchVersion = Integer.parseInt(matcherDefault.group(4));
this.extendVersion = 0;
this.customName = matcherDefault.group(5);
this.version = String.format("%s_V%s.%s.%s.%s", businessSpace, majorVersion, minorVersion, patchVersion, extendVersion);
} else {
Matcher matcherExtend = PTN_SCRIPT_NAME_EXTEND.matcher(fileName);
if (matcherExtend.matches()) {
this.businessSpace = matcherExtend.group(1);
this.majorVersion = Integer.parseInt(matcherExtend.group(2));
this.minorVersion = Integer.parseInt(matcherExtend.group(3));
this.patchVersion = Integer.parseInt(matcherExtend.group(4));
this.extendVersion = Integer.parseInt(matcherExtend.group(5));
this.customName = matcherExtend.group(6);
this.version = String.format("%s_V%s.%s.%s.%s", businessSpace, majorVersion, minorVersion, patchVersion, extendVersion);
} else {
throw new RuntimeException(fileName + " format is not correct!");
}
}
}
/**
* 检查该SQL脚本是否需要执行
*
* @param major 数据库版本控制表最新记录的主版本号
* @param minor 数据库版本控制表最新记录的次版本号
* @param patch 数据库版本控制表最新记录的补丁版本号
* @param extend 数据库版本控制表最新记录的扩展版本号
* @param versionType
* @param curHex
* @return 是否需要执行
*/
public boolean checkNeed(int major, int minor, int patch, int extend, String versionType, String curHex) {
if (this.majorVersion < major) {
return false;
} else if (this.majorVersion > major) {
return true;
}
if (this.minorVersion < minor) {
return false;
} else if (this.minorVersion > minor) {
return true;
}
if (this.patchVersion < patch) {
return false;
} else if (this.patchVersion > patch) {
return true;
}
if (this.extendVersion < extend) {
return false;
} else if (this.extendVersion > extend) {
return true;
}
if ("BaseLine".equals(versionType)) {
return false;
}
return !this.hex.equals(curHex);
}
@Override
public int compareTo(SQLScriptEntity o) {
if (this.getMajorVersion() == o.getMajorVersion()) {
if (this.getMinorVersion() == o.getMinorVersion()) {
if (this.getPatchVersion() == o.getPatchVersion()) {
return Integer.compare(this.getExtendVersion(), o.getExtendVersion());
} else {
return Integer.compare(this.getPatchVersion(), o.getPatchVersion());
}
} else {
return Integer.compare(this.getMinorVersion(), o.getMinorVersion());
}
} else {
return Integer.compare(this.getMajorVersion(), o.getMajorVersion());
}
}
/**
* 关闭SQL脚本的输入流
*/
public void closeInputStream() {
if (this.inputStream != null) {
try {
this.inputStream.close();
} catch (IOException e) {
logger.warn("closeInputStream fail! fileName:[" + this.fileName + "]", e);
}
}
}
@Override
public String toString() {
return "SQLScriptEntity{" +
"fileName='" + fileName + '\'' +
", businessSpace='" + businessSpace + '\'' +
", majorVersion=" + majorVersion +
", minorVersion=" + minorVersion +
", patchVersion=" + patchVersion +
", extendVersion=" + extendVersion +
", version='" + version + '\'' +
", customName='" + customName + '\'' +
'}';
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public InputStream getInputStream() {
return inputStream;
}
public void setInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}
public String getBusinessSpace() {
return businessSpace;
}
public void setBusinessSpace(String businessSpace) {
this.businessSpace = businessSpace;
}
public int getMajorVersion() {
return majorVersion;
}
public void setMajorVersion(int majorVersion) {
this.majorVersion = majorVersion;
}
public int getMinorVersion() {
return minorVersion;
}
public void setMinorVersion(int minorVersion) {
this.minorVersion = minorVersion;
}
public int getPatchVersion() {
return patchVersion;
}
public void setPatchVersion(int patchVersion) {
this.patchVersion = patchVersion;
}
public int getExtendVersion() {
return extendVersion;
}
public void setExtendVersion(int extendVersion) {
this.extendVersion = extendVersion;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getCustomName() {
return customName;
}
public void setCustomName(String customName) {
this.customName = customName;
}
public String getHex() {
return hex;
}
public void setHex(String hex) {
this.hex = hex;
}
}

View File

@@ -0,0 +1,54 @@
package com.codvision.db.versionctl.task;
import com.codvision.db.versionctl.chain.DbVersionCtlAbstractTask;
import com.codvision.db.versionctl.chain.DbVersionCtlContext;
import java.io.InputStream;
import java.util.List;
/**
* 任务:创建数据库版本控制表
*
* @author lingee
*/
@SuppressWarnings("unused")
public class CreateVersionTblTask extends DbVersionCtlAbstractTask {
public CreateVersionTblTask(DbVersionCtlContext context) {
super(context);
}
@Override
public boolean runTask() {
logger.info("CreateVersionTblTask begin...");
// 读取建表文
List<String> sqls = readCreateSql();
// 执行建表文
this.jdbcUtil.executeWithConnection(this.connection, sqls);
logger.info("CreateVersionTblTask end...");
return true;
}
private List<String> readCreateSql() {
String dbVersionTableCreateSqlPath = this.context.getDbVersionCtlProps().getDbVersionTableCreateSqlPath();
String dbVersionTableName = this.context.getDbVersionCtlProps().getDbVersionTableName();
boolean needReplaceTblName =
"classpath:db/versionctl/create_cvbp_db_version_ctl.sql".equals(dbVersionTableCreateSqlPath)
&& !"cvbp_db_version_ctl".equals(dbVersionTableName);
InputStream inputStream;
logger.debug("readCreateSql : {}", dbVersionTableCreateSqlPath);
if (dbVersionTableCreateSqlPath.startsWith("classpath:")) {
logger.debug("readCreateSql in classpath.");
inputStream = loadInputStreamFromClassPath(dbVersionTableCreateSqlPath);
} else {
logger.debug("readCreateSql in filesystem.");
inputStream = loadInputStreamFromFile(dbVersionTableCreateSqlPath);
}
if (inputStream != null) {
return readSqlFromInputStream(dbVersionTableName, needReplaceTblName, inputStream);
} else {
throw new RuntimeException(dbVersionTableCreateSqlPath + " not found!");
}
}
}

View File

@@ -0,0 +1,29 @@
package com.codvision.db.versionctl.task;
import com.codvision.db.versionctl.chain.DbVersionCtlAbstractTask;
import com.codvision.db.versionctl.chain.DbVersionCtlContext;
import lombok.extern.slf4j.Slf4j;
/**
* 任务:删除数据库版本控制表
*
* @author lingee
*/
@Slf4j
@SuppressWarnings("unused")
public class DropVersionTblTask extends DbVersionCtlAbstractTask {
public DropVersionTblTask(DbVersionCtlContext context) {
super(context);
}
@Override
public boolean runTask() {
log.info("DropVersionTblTask begin...");
String dbVersionTableName = this.context.getDbVersionCtlProps().getDbVersionTableName();
String dropTblSql = "drop table " + dbVersionTableName;
this.jdbcUtil.execute(dropTblSql);
log.info("DropVersionTblTask end...");
return true;
}
}

View File

@@ -0,0 +1,225 @@
package com.codvision.db.versionctl.task;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ClassLoaderUtil;
import com.codvision.db.enums.SysBusinessSpace;
import com.codvision.db.utils.ScriptReader;
import com.codvision.db.versionctl.chain.DbVersionCtlAbstractTask;
import com.codvision.db.versionctl.chain.DbVersionCtlContext;
import com.codvision.db.versionctl.entity.DbVersionEntity;
import com.codvision.db.versionctl.entity.SQLScriptEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import java.io.*;
import java.sql.SQLException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 任务:查找并执行增量SQL并记录数据库版本
*
* @author lingee
*/
@Slf4j
@SuppressWarnings("unused")
public class IncreaseVersionTask extends DbVersionCtlAbstractTask {
public IncreaseVersionTask(DbVersionCtlContext context) {
super(context);
}
@Override
public boolean runTask() {
logger.info("IncreaseVersionTask begin...");
// 生成数据库版本插入SQL语句
String insertSql = this.context.makeInsertSql();
// 获取数据库版本升级SQL脚本目录集合
List<String> scriptDirPaths = this.context.getDbVersionCtlProps().makeScriptDirPaths();
logger.debug("IncreaseVersionTask scriptDirPaths:{}", scriptDirPaths);
// 生成sql脚本对象集合
List<SQLScriptEntity> sqlByBs = createSqlScriptEntities(scriptDirPaths);
// 执行脚本升级数据库版本
increaseDbVersion(insertSql, sqlByBs);
// 关闭所有输入流
sqlByBs.forEach(SQLScriptEntity::closeInputStream);
logger.info("IncreaseVersionTask end...");
return true;
}
private void increaseDbVersion(String insertSql, List<SQLScriptEntity> sqlByBs) {
String updateSql = this.context.makeUpdateSql();
Map<String, List<SQLScriptEntity>> sqlGrpByBs = sqlByBs.stream()
.collect(Collectors.groupingBy(SQLScriptEntity::getBusinessSpace));
/* 系统相关服务数据库脚本需要按顺序执行 */
LinkedHashMap<String, List<SQLScriptEntity>> sqlGrpByBsLinked = new LinkedHashMap<>();
for (SysBusinessSpace sys : SysBusinessSpace.values()) {
if (CollectionUtil.isNotEmpty(sqlGrpByBs.get(sys.getCode()))) {
sqlGrpByBsLinked.put(sys.getCode(), sqlGrpByBs.get(sys.getCode()));
sqlGrpByBs.remove(sys.getCode());
}
}
if (CollectionUtil.isNotEmpty(sqlGrpByBs)) {
sqlGrpByBsLinked.putAll(sqlGrpByBs);
}
for (Map.Entry<String, List<SQLScriptEntity>> gp : sqlGrpByBsLinked.entrySet()) {
List<DbVersionEntity> dbVersionEntities = queryDbVersionEntities(gp.getKey());
List<SQLScriptEntity> sqlScriptEntities = gp.getValue();
if (!dbVersionEntities.isEmpty()) {
for (DbVersionEntity dbVersionEntity : dbVersionEntities) {
int curMajor = dbVersionEntity.getMajorVersion();
int curMinor = dbVersionEntity.getMinorVersion();
int curPatch = dbVersionEntity.getPatchVersion();
int curExtend = dbVersionEntity.getExtendVersion();
String versionType = dbVersionEntity.getVersionType();
String curHex = dbVersionEntity.getScriptDigestHex();
sqlScriptEntities = sqlScriptEntities.stream()
.filter(sqlScriptEntity -> sqlScriptEntity.checkNeed(curMajor, curMinor, curPatch, curExtend, versionType, curHex))
.collect(Collectors.toList());
}
}
if (sqlScriptEntities.isEmpty()) {
logger.info("业务空间 {} 没有增量sql脚本需要执行.", gp.getKey());
continue;
}
// 按版本排序
CollectionUtil.sort(sqlScriptEntities, SQLScriptEntity::compareTo);
sqlScriptEntities.forEach(sqlScriptEntity -> {
logger.info("增量执行脚本:{}", sqlScriptEntity.getFileName());
// 读取脚本内容
ScriptReader scriptReader = new ScriptReader(sqlScriptEntity.getInputStream());
List<String> sqls = scriptReader.readSqls();
// 开始时间
LocalDateTime startTime = LocalDateTime.now();
// 插入版本记录
this.jdbcUtil.execute(this.connection, insertSql,
sqlScriptEntity.getBusinessSpace(), sqlScriptEntity.getMajorVersion(),
sqlScriptEntity.getMinorVersion(), sqlScriptEntity.getPatchVersion(), sqlScriptEntity.getExtendVersion(),
sqlScriptEntity.getVersion(), sqlScriptEntity.getCustomName(),
"SQL", sqlScriptEntity.getFileName(), sqlScriptEntity.getHex(), 0, -1,
DateTimeFormatter.ofPattern(DbVersionCtlContext.DATETIME_PTN).format(startTime),
this.context.getDbVersionCtlProps().getUsername());
// 执行脚本
this.jdbcUtil.executeWithConnection(this.connection, sqls);
LocalDateTime stopTime = LocalDateTime.now();
long mills = Duration.between(startTime, stopTime).toMillis();
logger.info("sql脚本 {} 执行耗时 : {} ms.", sqlScriptEntity.getFileName(), mills);
// 更新版本记录
this.jdbcUtil.execute(this.connection, updateSql,
mills, sqlScriptEntity.getBusinessSpace(),
sqlScriptEntity.getMajorVersion(), sqlScriptEntity.getMinorVersion(),
sqlScriptEntity.getPatchVersion(), sqlScriptEntity.getExtendVersion());
logger.info("数据库版本记录更新, business_space: {} , major_version: {} , minor_version: {} , patch_version: {} , extend_version: {} .",
sqlScriptEntity.getBusinessSpace(), sqlScriptEntity.getMajorVersion(),
sqlScriptEntity.getMinorVersion(), sqlScriptEntity.getPatchVersion(), sqlScriptEntity.getExtendVersion());
});
}
}
private List<SQLScriptEntity> createSqlScriptEntities(List<String> scriptDirPaths) {
List<SQLScriptEntity> sqlByBs = new ArrayList<>();
try {
for (String scriptDirPath : scriptDirPaths) {
if (scriptDirPath.startsWith("classpath")) {
readSqlFromClassPath(sqlByBs, scriptDirPath);
} else {
readSqlFromFile(sqlByBs, scriptDirPath);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return sqlByBs;
}
private void readSqlFromClassPath(List<SQLScriptEntity> sqlByBs, String scriptDirPath) throws IOException {
String classPath = scriptDirPath.substring(scriptDirPath.indexOf(":") + 1);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
String pathPattern = scriptDirPath.endsWith("/") ? scriptDirPath + "*.sql" : scriptDirPath + "/*.sql";
Resource[] resources = resolver.getResources(pathPattern);
for (Resource resource : resources) {
if (!resource.getURL().getPath().startsWith("file:")) {
Resource resourcePath = new ClassPathResource(classPath + resource.getFilename());
sqlByBs.add(new SQLScriptEntity(resourcePath.getFilename(), resourcePath.getInputStream()));
} else {
InputStream inputStream = ClassLoaderUtil.getClassLoader().getResourceAsStream(classPath + resource.getFilename());
sqlByBs.add(new SQLScriptEntity(resource.getFilename(), inputStream));
}
}
}
private void readSqlFromFile(List<SQLScriptEntity> sqlByBs, String scriptDirPath) throws FileNotFoundException {
File folder = new File(scriptDirPath);
if (folder.exists() && folder.isDirectory()) {
File[] files = folder.listFiles((dir, name) -> name.endsWith(".sql"));
if (files == null || files.length == 0) {
throw new RuntimeException("There is no sql files in [" + scriptDirPath + "]!");
}
for (File file : files) {
sqlByBs.add(new SQLScriptEntity(file.getName(), new FileInputStream(file)));
}
} else {
throw new RuntimeException(scriptDirPath + " is not Filesystem Directory!");
}
}
private List<DbVersionEntity> queryDbVersionEntities(String bs) {
String selectSql = this.context.makeSelectSql();
List<DbVersionEntity> dbVersionEntities = new ArrayList<>();
try {
this.jdbcUtil.getRunner().query(this.connection, selectSql, resultSet -> {
while (resultSet.next()) {
DbVersionEntity dbVersionEntity = new DbVersionEntity();
dbVersionEntity.setId(resultSet.getInt("id"));
dbVersionEntity.setBusinessSpace(resultSet.getString("business_space"));
dbVersionEntity.setMajorVersion(resultSet.getInt("major_version"));
dbVersionEntity.setMinorVersion(resultSet.getInt("minor_version"));
dbVersionEntity.setPatchVersion(resultSet.getInt("patch_version"));
dbVersionEntity.setExtendVersion(resultSet.getInt("extend_version"));
dbVersionEntity.setVersion(resultSet.getString("version"));
dbVersionEntity.setCustomName(resultSet.getString("version"));
dbVersionEntity.setVersionType(resultSet.getString("version_type"));
dbVersionEntity.setScriptFileName(resultSet.getString("script_file_name"));
dbVersionEntity.setScriptDigestHex(resultSet.getString("script_digest_hex"));
dbVersionEntity.setSuccess(resultSet.getByte("success"));
dbVersionEntity.setExecutionTime(resultSet.getInt("execution_time"));
dbVersionEntity.setInstallTime(resultSet.getString("install_time"));
dbVersionEntity.setInstallUser(resultSet.getString("install_user"));
dbVersionEntities.add(dbVersionEntity);
}
return null;
}, bs);
} catch (SQLException e) {
throw new RuntimeException(e);
}
return dbVersionEntities;
}
}

View File

@@ -0,0 +1,91 @@
package com.codvision.db.versionctl.task;
import cn.hutool.core.util.StrUtil;
import com.codvision.db.versionctl.chain.DbVersionCtlAbstractTask;
import com.codvision.db.versionctl.chain.DbVersionCtlContext;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 任务:插入数据库基线版本记录
*
* @author lingee
*/
@SuppressWarnings("unused")
public class InsertBaselineTask extends DbVersionCtlAbstractTask {
private static final Pattern PTN_VERSION_DEFAULT = Pattern.compile("^([A-Za-z0-9]+)_V(\\d+)\\.(\\d+)\\.(\\d+)$");
private static final Pattern PTN_VERSION_EXTEND = Pattern.compile("^([A-Za-z0-9]+)_V(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)$");
private final List<String> businessSpaces = new ArrayList<>();
private final List<String> bsVersions = new ArrayList<>();
public InsertBaselineTask(DbVersionCtlContext context) {
super(context);
String baselineBusinessSpaceAndVersions = context.getDbVersionCtlProps().getBaselineBusinessSpaceAndVersions();
logger.debug("DbVersionCtlProps.baselineBusinessSpaceAndVersions : {}", baselineBusinessSpaceAndVersions);
if (StrUtil.isBlank(baselineBusinessSpaceAndVersions)) {
throw new RuntimeException("DbVersionCtlProps.baselineBusinessSpaceAndVersions is empty!");
}
String[] arrTmp = baselineBusinessSpaceAndVersions.split(",");
for (String bsAndVersion : arrTmp) {
if (StrUtil.isBlank(bsAndVersion)) {
continue;
}
String[] bsAndVersionArr = StrUtil.trim(bsAndVersion).split("_");
if (bsAndVersionArr.length != 2) {
throw new RuntimeException("DbVersionCtlProps.baselineBusinessSpaceAndVersions format is not correct!");
}
this.businessSpaces.add(StrUtil.trim(bsAndVersionArr[0]));
this.bsVersions.add(bsAndVersion);
}
}
@Override
public boolean runTask() {
logger.info("InsertBaselineTask begin...");
String insertSql = this.context.makeInsertSql();
logger.debug("BaseLine InsertSQL : [{}]", insertSql);
for (int i = 0; i < this.businessSpaces.size(); i++) {
String bs = this.businessSpaces.get(i);
String version = this.bsVersions.get(i);
Matcher matcherDefault = PTN_VERSION_DEFAULT.matcher(version);
if (matcherDefault.matches()) {
int major = Integer.parseInt(matcherDefault.group(2));
int minor = Integer.parseInt(matcherDefault.group(3));
int patch = Integer.parseInt(matcherDefault.group(4));
int extend = 0;
this.jdbcUtil.execute(this.connection, insertSql,
bs, major, minor, patch, extend, version, "none", "BaseLine", "none", "none", 1, 0,
DateTimeFormatter.ofPattern(DbVersionCtlContext.DATETIME_PTN).format(LocalDateTime.now()),
this.context.getDbVersionCtlProps().getUsername());
logger.info("数据库基线版本添加, business_space: {} , major_version: {} , minor_version: {} , patch_version: {}, extend_version: {} .",
bs, major, minor, patch, extend);
} else {
Matcher matcherExtend = PTN_VERSION_EXTEND.matcher(version);
if (matcherExtend.matches()) {
int major = Integer.parseInt(matcherExtend.group(2));
int minor = Integer.parseInt(matcherExtend.group(3));
int patch = Integer.parseInt(matcherExtend.group(4));
int extend = Integer.parseInt(matcherExtend.group(5));
this.jdbcUtil.execute(this.connection, insertSql,
bs, major, minor, patch, extend, version, "none", "BaseLine", "none", "none", 1, 0,
DateTimeFormatter.ofPattern(DbVersionCtlContext.DATETIME_PTN).format(LocalDateTime.now()),
this.context.getDbVersionCtlProps().getUsername());
logger.info("数据库基线版本添加, business_space: {} , major_version: {} , minor_version: {} , patch_version: {}, extend_version: {} .",
bs, major, minor, patch, extend);
} else {
throw new RuntimeException("DbVersionCtlProps.baselineBusinessSpaceAndVersions format is not correct!");
}
}
}
logger.info("InsertBaselineTask end...");
return true;
}
}

View File

@@ -0,0 +1,55 @@
package com.codvision.db.versionctl.task;
import com.codvision.db.versionctl.chain.DbVersionCtlAbstractTask;
import com.codvision.db.versionctl.chain.DbVersionCtlContext;
import java.io.InputStream;
import java.util.List;
/**
* 任务:修改数据库版本控制表
*
* @author lingee
*/
@SuppressWarnings("unused")
public class ModifyVersionTblTask extends DbVersionCtlAbstractTask {
public ModifyVersionTblTask(DbVersionCtlContext context) {
super(context);
}
@Override
public boolean runTask() {
logger.info("ModifyVersionTblTask begin...");
// 读取修改SQL文
List<String> sqls = readModifySql();
// 执行建表文
this.jdbcUtil.executeWithConnection(this.connection, sqls);
logger.info("ModifyVersionTblTask end...");
return true;
}
@SuppressWarnings("squid:S112")
private List<String> readModifySql() {
String modifyDbVersionTableSqlPath = this.context.getDbVersionCtlProps().getModifyDbVersionTableSqlPath();
String dbVersionTableName = this.context.getDbVersionCtlProps().getDbVersionTableName();
boolean needReplaceTblName =
"classpath:db/versionctl/modify_cvbp_db_version_ctl.sql".equals(modifyDbVersionTableSqlPath)
&& !"cvbp_db_version_ctl".equals(dbVersionTableName);
InputStream inputStream;
logger.debug("readModifySql : {}", modifyDbVersionTableSqlPath);
if (modifyDbVersionTableSqlPath.startsWith("classpath:")) {
logger.debug("readModifySql in classpath.");
inputStream = loadInputStreamFromClassPath(modifyDbVersionTableSqlPath);
} else {
logger.debug("readCreateSql in filesystem.");
inputStream = loadInputStreamFromFile(modifyDbVersionTableSqlPath);
}
if (inputStream != null) {
return readSqlFromInputStream(dbVersionTableName, needReplaceTblName, inputStream);
} else {
throw new RuntimeException(modifyDbVersionTableSqlPath + " not found!");
}
}
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.codvision.db.config.DBConfig,\
com.codvision.db.datasource.CacheDataSourceFactoryBean

View File

@@ -0,0 +1,36 @@
DROP TABLE IF EXISTS cvbp_db_version_ctl;
CREATE TABLE cvbp_db_version_ctl(
id SERIAL NOT NULL,
business_space VARCHAR(50) NOT NULL,
major_version int4 NOT NULL,
minor_version int4 NOT NULL,
patch_version int4 NOT NULL,
extend_version int4 NOT NULL DEFAULT 0,
version VARCHAR(50) NOT NULL,
custom_name VARCHAR(50) NOT NULL DEFAULT 'none',
version_type VARCHAR(10) NOT NULL,
script_file_name VARCHAR(200) NOT NULL DEFAULT 'none',
script_digest_hex VARCHAR(200) NOT NULL DEFAULT 'none',
success int2 NOT NULL,
execution_time int4 NOT NULL,
install_time VARCHAR(19) NOT NULL,
install_user VARCHAR(100) NOT NULL,
PRIMARY KEY (id)
);
COMMENT ON TABLE cvbp_db_version_ctl IS '数据库版本控制表';
COMMENT ON COLUMN cvbp_db_version_ctl.id IS '数据库版本ID';
COMMENT ON COLUMN cvbp_db_version_ctl.business_space IS '业务空间';
COMMENT ON COLUMN cvbp_db_version_ctl.major_version IS '主版本号';
COMMENT ON COLUMN cvbp_db_version_ctl.minor_version IS '次版本号';
COMMENT ON COLUMN cvbp_db_version_ctl.patch_version IS '补丁版本号';
COMMENT ON COLUMN cvbp_db_version_ctl.extend_version IS '扩展版本号';
COMMENT ON COLUMN cvbp_db_version_ctl.version IS '版本号,V[major].[minor].[patch].[extend_version]';
COMMENT ON COLUMN cvbp_db_version_ctl.custom_name IS '脚本自定义名称';
COMMENT ON COLUMN cvbp_db_version_ctl.version_type IS '版本类型:SQL/BaseLine';
COMMENT ON COLUMN cvbp_db_version_ctl.script_file_name IS '脚本文件名';
COMMENT ON COLUMN cvbp_db_version_ctl.script_digest_hex IS '脚本内容摘要(16进制)';
COMMENT ON COLUMN cvbp_db_version_ctl.success IS '是否执行成功';
COMMENT ON COLUMN cvbp_db_version_ctl.execution_time IS '脚本安装耗时';
COMMENT ON COLUMN cvbp_db_version_ctl.install_time IS '脚本安装时间,格式:[yyyy-MM-dd HH:mm:ss]';
COMMENT ON COLUMN cvbp_db_version_ctl.install_user IS '脚本安装用户';