1. 问题背景
利用业余时间开发已经过了一个月了,目前完成了设计的前半部分:数据源管理、数据源分组管理、数据集管理 。当然还只是初步功能实现,并且只支持MYSQL数据库。
记录下这个月碰到的问题:
数据库连接信息来自用户配置,若依框架用的是MyBatis。使用什么方式连接数据源成了最先面对的问题。
因此考虑了两个方案:
- 模拟使用MyBatis,切换数据源。并动态生成Mapper文件注册到MyBatis中完成入参和出参的映射。做过各种尝试失败了。
- 使用JDBC完成。但是复杂的字符串拼接工作低级低效。在前面MyBatis文章 MyBatis08-《通用源码指导书:MyBatis源码详解》笔记-JDBC和cache包 中有介绍SqlRunner类。比较完美的解决了我的需求。
注意:这里说的动态数据源不是在配置文件中 预先配置好数据源,再通过手动或者注解切换数据源完成数据的CRUD操作。这里的动态数据源是指:根据用户配置数据链接和账号信息,读取该数据库所有表结构,并动态读取数据
下面就对SqlRunner在项目中的使用做个介绍:
2.先写一个简单的SqlRunner示例
public static void main(String[] args) throws SQLException {
// url: jdbc:mysql://127.0.0.1:3306/data_generation?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
// username: root
// password: naruto1012
DResource dResource = new DResource();
dResource.setResourcePassword("naruto1012");
dResource.setResourceUsername("root");
dResource.setResourceName("数据生成平台测试SqlRunner用法");
dResource.setResourceUrl("127.0.0.1");
dResource.setResourcePort("3306");
dResource.setResourceDatabase("data_generation");
SqlRunner sqlRunner = getSqlRunner(dResource);
//测试连接是否可用
Map<String, Object> objectMap = sqlRunner.selectOne(testSqlStaic);
System.out.println("测试连接查询:"+JSON.toJSONString(objectMap));
//写一个简单的查询 不用遵循 select from where group order limit 顺序可随意拼接
SQL sql = new SQL();
String[] columnArr = {"resource_id,resource_name,resource_desc"};
sql.SELECT(columnArr).LIMIT(2);
sql.WHERE("resource_type=?");
sql.FROM("d_resource") ;
List<Map<String, Object>> maps = sqlRunner.selectAll(sql.toString(), 1);
System.out.println("查d_resource表:"+JSON.toJSONString(maps));
}
private final static String testSqlStaic = "select 1 from dual";
/**
* 创建SqlRunner
* @param dResource
* @return
*/
public static SqlRunner getSqlRunner(DResource dResource) {
try {
return new SqlRunner(getDataSource(dResource).getConnection());
} catch (SQLException e) {
logger.error("数据源[{}]链接失败!",dResource.getResourceName(),e);
removeCache(dResource.getResourceId());
throw new ServiceException("数据源["+dResource.getResourceName()+"]链接失败!");
}
}
/**
* 创建dataSource
* @param resource
* @return
*/
private static DataSource getDataSource(DResource resource){
String url = "jdbc:%s://%s:%s/%s?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true";
url = String.format(url,"mysql", resource.getResourceUrl(), resource.getResourcePort(), resource.getResourceDatabase());
PooledDataSource dataSource = new PooledDataSource(
"com.mysql.cj.jdbc.Driver",
url,
resource.getResourceUsername(),
resource.getResourcePassword());
dataSource.setLoginTimeout(3);
dataSource.setDefaultNetworkTimeout(10000);
return dataSource;
}
这里需要一个DResource实体类:
package com.huazige.system.domain;
import com.huazige.common.annotation.Excel;
import com.huazige.common.core.domain.BaseEntity;
/**
* 数据源对象 d_resource
*
* @author huazige
* @date 2023-07-23
*/
public class DResource
{
private static final long serialVersionUID = 1L;
/** */
private String resourceId;
/** 数据源名 */
@Excel(name = "数据源名")
private String resourceName;
/** 数据源描述 */
@Excel(name = "数据源描述")
private String resourceDesc;
/** 数据源类型:1 mysql 2 oracle */
@Excel(name = "数据源类型:1 mysql 2 oracle")
private Long resourceType;
/** 数据源url */
@Excel(name = "数据源url")
private String resourceUrl;
/** 数据源数据库 */
@Excel(name = "数据源数据库")
private String resourceDatabase;
/** 数据源端口 */
@Excel(name = "数据源端口")
private String resourcePort;
/** 数据源账号 */
@Excel(name = "数据源账号")
private String resourceUsername;
/** 数据源密码 */
@Excel(name = "数据源密码")
private String resourcePassword;
/** 0正常 1停用 */
@Excel(name = "0正常 1停用")
private String state;
/** 0正常 1删除 */
@Excel(name = "0正常 1删除")
private String del;
运行结果:
11:39:09.936 [main] DEBUG org.apache.ibatis.logging.LogFactory - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
11:39:10.017 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
11:39:10.824 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 961712517.
测试连接查询:{"1":1}
查d_resource表:[{"RESOURCE_ID":1,"RESOURCE_NAME":"local-mysql-data-generation","RESOURCE_DESC":"本地连接mysql-data-generation"},{"RESOURCE_ID":2,"RESOURCE_NAME":"localhost","RESOURCE_DESC":"本地mysql"}]
从结果上看。SqlRunner配合SQL类可以非常简单的解决原始JDBC复杂的sql语句拼接和数据连接connection繁杂的开启关闭。并且参数的拼接也非常简单。
List<Map<String, Object>> maps = sqlRunner.selectAll(sql.toString(), 1);
这里的最后一个参数’1’就是传入的参数,对应
sql.WHERE("resource_type=?");
基于此,再来介绍’数据生成平台’中对SqlRunner和SQL的应用
3.'数据生成平台’中对SqlRunner和SQL的应用
package com.huazige.system.executor;
import com.huazige.common.core.domain.AjaxResult;
import com.huazige.common.enums.DisableStatus;
import com.huazige.common.exception.ServiceException;
import com.huazige.common.utils.StringUtils;
import com.huazige.system.domain.DResource;
import java.sql.SQLException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
import javax.sql.DataSource;
import org.apache.ibatis.datasource.pooled.PooledDataSource;
import org.apache.ibatis.jdbc.SqlRunner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @ClassName Execute
* @Description sql执行
* @Author Hzg
* @Date 2023/8/5 17:48
* @Version 1.0
*/
public class SQLExecutor {
private static final Logger logger = LoggerFactory.getLogger(SQLExecutor.class);
private static ConcurrentHashMap<String, SQLExecutor> executeMap = new ConcurrentHashMap<>();
private DResource dResource ;
private final String testSql = "select 1 from dual";
/**
* 是否缓存数据源
*/
private Boolean cache;
private SQLExecutor(DResource dResource,Boolean cache) {
this.dResource=dResource;
this.cache =cache;
}
public static void removeCache(String resourceId){
if(executeMap.contains(resourceId)){
executeMap.remove(resourceId);
}
}
public static SQLExecutor getInstance(DResource dResource,Boolean cache){
cache = cache==null?false:cache;
if(dResource==null || StringUtils.equals(DisableStatus.DISABLE.getCode(),dResource.getState())){
logger.error("获取sql执行器失败!dResource不能为空!");
throw new ServiceException("获取sql执行器失败!数据源为空或已停用!");
}
if(cache){
SQLExecutor sqlExecute = executeMap.get(dResource.getResourceId());
if(sqlExecute !=null){
return sqlExecute;
}
}
ReentrantLock lock = new ReentrantLock();
lock.lock();
SQLExecutor execute = new SQLExecutor(dResource,cache);
if(cache){
executeMap.put(dResource.getResourceId(),execute);
}
lock.unlock();
return execute;
}
public AjaxResult testConnection() {
try {
getSqlRunner().selectOne(testSql);
} catch (SQLException e) {
logger.error("数据源[{}]链接测试失败!",dResource.getResourceName());
removeCache(dResource.getResourceId());
throw new RuntimeException("链接失败!",e);
}
return AjaxResult.success("测试成功");
}
private DataSource getDataSource(){
String url = "jdbc:%s://%s:%s/%s?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true";
url = String.format(url,"mysql", dResource.getResourceUrl(), dResource.getResourcePort(), dResource.getResourceDatabase());
PooledDataSource dataSource = new PooledDataSource(
"com.mysql.cj.jdbc.Driver",
url,
dResource.getResourceUsername(),
dResource.getResourcePassword());
dataSource.setLoginTimeout(3);
dataSource.setDefaultNetworkTimeout(10000);
return dataSource;
}
public SqlRunner getSqlRunner() {
try {
return new SqlRunner(getDataSource().getConnection());
} catch (SQLException e) {
logger.error("数据源[{}]链接失败!",dResource.getResourceName(),e);
removeCache(dResource.getResourceId());
throw new ServiceException("数据源["+dResource.getResourceName()+"]链接失败!");
}
}
}
最后 配合org.apache.ibatis.jdbc.SQL 类,基本解决动态查询问题:
package com.huazige.system.executor;
import com.huazige.common.core.page.PageDomain;
import com.huazige.common.enums.DataSetConvertType;
import com.huazige.system.domain.DDataSet;
import com.huazige.system.domain.DDataSetMeta;
import com.huazige.system.domain.DResource;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.commons.compress.utils.Lists;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.ibatis.jdbc.SQL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @ClassName SQLAssembleStrategy
* @Description
* @Author Hzg
* @Date 2023/8/13 14:15
* @Version 1.0
*/
public abstract class SQLAssembleTemplate {
private static final Logger logger = LoggerFactory.getLogger(SQLAssembleTemplate.class);
private List<DDataSetMeta> dataSetMetas;
private DDataSet dataSet;
private DResource resource;
private String dataBaseType;
private SQL sql = new SQL();
public SQLAssembleTemplate(List<DDataSetMeta> dataSetMetas, String dataBaseType,DDataSet dataSet,DResource resource) {
this.dataSetMetas = dataSetMetas;
this.dataBaseType = dataBaseType;
this.dataSet = dataSet;
this.resource=resource;
}
private String getQueryColumnSql(DDataSetMeta dataSetMeta) {
DataSetConvertType dataSetConvertType = DataSetConvertType.getByType(dataSetMeta.getConvertType());
String columnSql = "";
switch (dataSetConvertType) {
case CUSTOM:
columnSql = dataSetMeta.getExpression();
break;
case CONVERT:
columnSql = typeConvert(dataSetMeta);
break;
case PROTOGENESIS:
columnSql = dataSetMeta.getSourceColumnName();
break;
}
columnSql=columnSql + " AS "+dataSetMeta.getTargetColumnName();
return columnSql;
}
/**
* 列转换
* @param dataSetMeta
* @return
*/
protected abstract String typeConvert(DDataSetMeta dataSetMeta);
/**
* 获取查询列
* @return
*/
private List<String> getQueryColumnSql(){
List<String> columnList = new ArrayList<>();
for (DDataSetMeta dataSetMeta:dataSetMetas) {
columnList.add(getQueryColumnSql(dataSetMeta));
}
return columnList;
}
private String getForm(){
return dataSet.getTableName();
}
/**
* 执行查询
* @return
*/
public List<Map<String, Object>> list() throws SQLException {
String[] columnArray = getQueryColumnSql().toArray(new String[0]);
sql.SELECT(columnArray).FROM(getForm());
String sqlStr = sql.toString();
logger.info("sql={}",sqlStr);
return SQLExecutor.getInstance(resource,true).getSqlRunner().selectAll(sqlStr);
}
public List<Map<String, Object>> page(PageDomain page) throws SQLException {
String[] columnArray = getQueryColumnSql().toArray(new String[0]);
sql.SELECT(columnArray).FROM(getForm());
Integer count = getCount();
page.setTotal(count);
if(count<1){
return Lists.newArrayList();
}
if(page!=null){
setPage(page);
}
String sqlStr = sql.toString();
logger.info("sql={}",sqlStr);
return SQLExecutor.getInstance(resource,true).getSqlRunner().selectAll(sqlStr);
}
private final String COUNT="COUNT";
private Integer getCount() throws SQLException {
SQL countSql = new SQL().SELECT("COUNT(*) "+ COUNT).FROM("("+sql.toString()+") a ");
String countSqlStr = countSql.toString();
logger.info("countSql={}",countSqlStr);
Map<String, Object> objectMap = SQLExecutor.getInstance(resource, true).getSqlRunner().selectOne(countSqlStr);
Object o = objectMap.get(COUNT);
return o==null?0: NumberUtils.toInt(o.toString());
}
/**
*设置分页查询
*/
private void setPage(PageDomain page){
sql.OFFSET(Math.toIntExact(page.getStartRow())).LIMIT(page.getPageSize());
}
}
4.已实现功能截图:
4.1.连接测试:
4.2.数据源分组
4.3.数据源列表
4.4.从数据库表创建数据源(从数据源读取表结构)
4.5.数据集创建(自定义转换)
4.6.预览数据集数据
5.动态数据源剩下的问题:
剩下的问题就是解析查询结果,映射到实体类了。MyBatis花了大力气解决此问题,当然MyBatis需要兼容多种情况。包括关联查询,一对多,多对多。而我从这个月看还没有用到要大量解析需求。后面如果有此需求再去探索下是否可以利用MyBatis已写好的解析类来解决此问题。