#feat: 动态数据源模块

This commit is contained in:
ixyxj 2024-05-06 10:59:57 +08:00
parent f7f2c1730d
commit e1b221289d
29 changed files with 2350 additions and 0 deletions

View File

@ -389,6 +389,13 @@
<version>${revision}</version>
</dependency>
<!--数据源模块-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-datasource</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@ -81,6 +81,12 @@
<artifactId>ruoyi-workflow</artifactId>
</dependency>
<!--数据源模块-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-datasource</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>

View File

@ -15,6 +15,7 @@
<module>ruoyi-job</module>
<module>ruoyi-system</module>
<module>ruoyi-workflow</module>
<module>ruoyi-datasource</module>
</modules>
<artifactId>ruoyi-modules</artifactId>

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-modules</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<packaging>jar</packaging>
<artifactId>ruoyi-datasource</artifactId>
<properties>
<clickhouse.version>0.6.0</clickhouse.version>
</properties>
<description>
数据源模块
</description>
<dependencies>
<!--通用工具-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-idempotent</artifactId>
</dependency>
<!--数据mybatis-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-log</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-excel</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-web</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-tenant</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-db</artifactId>
</dependency>
<dependency>
<groupId>com.clickhouse</groupId>
<artifactId>clickhouse-jdbc</artifactId>
<version>${clickhouse.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,226 @@
package org.dromara.datasource.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.datasource.domain.bo.DsFieldQueryBo;
import org.dromara.datasource.domain.bo.DsQueryBo;
import org.dromara.datasource.domain.bo.SysDatasourceBo;
import org.dromara.datasource.domain.vo.SysDatasourceVo;
import org.dromara.datasource.jdbc.JdbcQuery;
import org.dromara.datasource.jdbc.core.DbType;
import org.dromara.datasource.service.ISysDatasourceService;
import org.dromara.datasource.utils.Assert;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* 动态数据源
*
* @author ixyxj
* @date 2024-05-02
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/system/datasource")
public class SysDatasourceController extends BaseController {
private final ISysDatasourceService sysDatasourceService;
/**
* 查询动态数据源列表
*/
@SaCheckPermission("system:datasource:list")
@GetMapping("/list")
public TableDataInfo<SysDatasourceVo> list(SysDatasourceBo bo, PageQuery pageQuery) {
return sysDatasourceService.queryPageList(bo, pageQuery);
}
/**
* 导出动态数据源列表
*/
@SaCheckPermission("system:datasource:export")
@Log(title = "动态数据源", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(SysDatasourceBo bo, HttpServletResponse response) {
List<SysDatasourceVo> list = sysDatasourceService.queryList(bo);
ExcelUtil.exportExcel(list, "动态数据源", SysDatasourceVo.class, response);
}
/**
* 获取动态数据源详细信息
*
* @param id 主键
*/
@SaCheckPermission("system:datasource:query")
@GetMapping("/{id}")
public R<SysDatasourceVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(sysDatasourceService.queryById(id));
}
/**
* 新增动态数据源
*/
@SaCheckPermission("system:datasource:add")
@Log(title = "动态数据源", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody SysDatasourceBo bo) {
return toAjax(sysDatasourceService.insertByBo(bo));
}
/**
* 修改动态数据源
*/
@SaCheckPermission("datasource:datasource:edit")
@Log(title = "动态数据源", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody SysDatasourceBo bo) {
return toAjax(sysDatasourceService.updateByBo(bo));
}
/**
* 删除动态数据源
*
* @param ids 主键串
*/
@SaCheckPermission("system:datasource:remove")
@Log(title = "动态数据源", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(sysDatasourceService.deleteWithValidByIds(List.of(ids), true));
}
/**
* 获取数据源类型
*/
@SaCheckPermission("system:datasource:query")
@GetMapping("/types")
public R<DbType[]> types() {
return R.ok(DbType.values());
}
/**
* 获取数据库列表
*/
@SaCheckPermission("system:datasource:query")
@PostMapping("/databases")
public R<List<String>> databases(@Validated @RequestBody DsQueryBo bo) {
List<String> query = sysDatasourceService.getJdbcService().query(bo.getDsName(), bo.getDsType(), JdbcQuery::queryDatabases);
return R.ok(query);
}
/**
* 获取表列表
*/
@SaCheckPermission("system:datasource:query")
@PostMapping("/tables")
public R<List<Map<String, Object>>> tables(@Validated @RequestBody DsQueryBo bo) {
List<Map<String, Object>> query = sysDatasourceService.getJdbcService().query(bo.getDsName(), bo.getDsType(), jdbcQuery -> jdbcQuery.querySchemaTables(bo.getSchema(), null));
return R.ok(query);
}
/**
* 获取字段列表
*/
@SaCheckPermission("system:datasource:query")
@PostMapping("/columns")
public R<List<Map<String, Object>>> columns(@Validated @RequestBody DsQueryBo bo) {
List<Map<String, Object>> query = sysDatasourceService.getJdbcService().query(bo.getDsName(), bo.getDsType(), jdbcQuery -> jdbcQuery.queryTableFields(bo.getSchema(), bo.getTable()));
return R.ok(query);
}
/**
* 查询sql脚本
*/
@SaCheckPermission("system:datasource:query")
@PostMapping("/query")
public R<Object> query(@Validated @RequestBody DsQueryBo bo) {
String sqlScript = StrUtil.removeSuffix(bo.getSqlScript(), ";");
Assert.isTrue(StrUtil.isNotBlank(sqlScript), "sql脚本不能为空");
List<?> query = sysDatasourceService.getJdbcService().query(bo.getDsName(), bo.getDsType(),
jdbcQuery -> {
JdbcTemplate jdbc = jdbcQuery.getJdbc();
if (StrUtil.isNotEmpty(bo.getSchema())) {
jdbc.execute("use `%s`".formatted(bo.getSchema()));
}
if (StrUtil.contains(sqlScript, ";")) {
return Arrays.stream(sqlScript.split(";"))
.distinct()
.filter(StrUtil::isNotBlank)
.map(String::trim)
.map(jdbc::queryForList)
.toList();
} else {
return jdbc.queryForList(sqlScript);
}
});
return R.ok(query);
}
/**
* 查询sql脚本
*/
@SaCheckPermission("system:datasource:query")
@PostMapping("/execute")
public R<List<Map<String, Object>>> execute(@Validated @RequestBody DsQueryBo bo) {
String sqlScript = StrUtil.removeSuffix(bo.getSqlScript(), ";");
Assert.isTrue(StrUtil.isNotBlank(sqlScript), "sql脚本不能为空");
sysDatasourceService.getJdbcService().execute(bo.getDsName(), bo.getDsType(),
jdbcQuery -> {
JdbcTemplate jdbc = jdbcQuery.getJdbc();
if (StrUtil.isNotEmpty(bo.getSchema())) {
jdbc.execute("use `%s`".formatted(bo.getSchema()));
}
Arrays.stream(sqlScript.split(";"))
.distinct()
.filter(StrUtil::isNotBlank)
.map(String::trim)
.forEach(jdbc::execute);
});
return R.ok("执行成功");
}
/**
* 测试数据源连接
*/
@SaCheckPermission("system:datasource:query")
@PostMapping("/test")
public R<Boolean> testConn(@Validated @RequestBody SysDatasourceBo bo) {
return R.ok(sysDatasourceService.testConnByBo(bo));
}
/**
* 查询字段数据
*/
@SaCheckPermission("system:datasource:query")
@PostMapping("/fieldData")
public R<Object> queryFieldData(@Validated @RequestBody DsFieldQueryBo bo) {
return R.ok(sysDatasourceService.queryFieldData(bo));
}
}

View File

@ -0,0 +1,74 @@
package org.dromara.datasource.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.tenant.core.TenantEntity;
import java.io.Serial;
/**
* 动态数据源对象 sys_datasource
*
* @author ixyxj
* @date 2024-05-02
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_datasource")
public class SysDatasource extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id")
private Long id;
/**
* 数据源类型
*/
private String dsType;
/**
* 数据源名称
*/
private String dsName;
/**
* 数据源url
*/
private String connUrl;
/**
* 数据源用户名
*/
private String username;
/**
* 数据源密码
*/
private String password;
/**
* 驱动类名
*/
private String driverClassName;
/**
* 删除标志0代表存在 2代表删除
*/
@TableLogic
private String delFlag;
/**
* 备注
*/
private String remark;
}

View File

@ -0,0 +1,29 @@
package org.dromara.datasource.domain.bo;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 数据源数据查询请求
*
* @author xyxj xieyangxuejun@gmail.com
* @since 2024/5/2
*/
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DsFieldQueryBo extends FieldQueryBo{
@NotBlank(message = "数据源名称不能为空")
private String dsName;
@NotBlank(message = "数据库名称不能为空")
private String schema;
@NotBlank(message = "数据表名称不能为空")
private String table;
}

View File

@ -0,0 +1,30 @@
package org.dromara.datasource.domain.bo;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 数据源查询请求
*
* @author xyxj xieyangxuejun@gmail.com
* @since 2024/5/2
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DsQueryBo {
@NotBlank(message = "数据源名称不能为空")
private String dsName;
@NotBlank(message = "数据源类型不能为空")
private String dsType;
private String schema;
private String table;
private String sqlScript;
}

View File

@ -0,0 +1,67 @@
package org.dromara.datasource.domain.bo;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Collection;
import java.util.List;
/**
* 字段查询请求
*
* @author xyxj xieyangxuejun@gmail.com
* @since 2024/5/2
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FieldQueryBo {
@NotEmpty(message = "字段不为空")
private Collection<String> columns;
private List<FieldCondition> fieldConditions;
private List<FieldOrder> fieldOrders;
private List<Integer> offsets;
private Integer limit;
private List<String> groupColumns;
@Data
public static class FieldCondition {
@NotEmpty(message = "字段名不能为空")
private String field;
private String type;
@NotEmpty(message = "运算符不能为空")
private String operator;
private String linkOperator;
@NotNull(message = "查询数据不能为空")
private Object value;
private Object secondValue;
}
@Data
public static class FieldOrder {
/**
* 排序的字段
*/
@NotEmpty(message = "字段名不能为空")
private String field;
/**
* 排序方式正序还是反序
*/
private String direction;
}
}

View File

@ -0,0 +1,76 @@
package org.dromara.datasource.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.datasource.domain.SysDatasource;
/**
* 动态数据源业务对象 sys_datasource
*
* @author ixyxj
* @date 2024-05-02
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = SysDatasource.class, reverseConvertGenerate = false)
public class SysDatasourceBo extends BaseEntity {
/**
* 主键
*/
@NotNull(message = "主键不能为空", groups = { EditGroup.class })
private Long id;
/**
* 数据源类型
*/
@NotBlank(message = "数据源类型不能为空", groups = { AddGroup.class, EditGroup.class })
private String dsType;
/**
* 数据源名称
*/
@NotBlank(message = "数据源名称不能为空", groups = { EditGroup.class })
private String dsName;
@NotBlank(message = "数据源host不能为空", groups = { EditGroup.class })
private String host;
@NotBlank(message = "数据源端口不能为空", groups = { EditGroup.class })
private String port;
/**
* 数据源url
*/
@NotBlank(message = "数据源url不能为空", groups = { AddGroup.class, EditGroup.class })
private String connUrl;
/**
* 数据源用户名
*/
private String username;
/**
* 数据源密码
*/
private String password;
/**
* 驱动类名
*/
@NotBlank(message = "驱动类名不能为空", groups = { AddGroup.class, EditGroup.class })
private String driverClassName;
/**
* 备注
*/
private String remark;
}

View File

@ -0,0 +1,77 @@
package org.dromara.datasource.domain.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.dromara.datasource.domain.SysDatasource;
import java.io.Serial;
import java.io.Serializable;
/**
* 动态数据源视图对象 sys_datasource
*
* @author ixyxj
* @date 2024-05-02
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = SysDatasource.class)
public class SysDatasourceVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@ExcelProperty(value = "主键")
private Long id;
/**
* 数据源类型
*/
@ExcelProperty(value = "数据源类型")
private String dsType;
/**
* 数据源名称
*/
@ExcelProperty(value = "数据源名称")
private String dsName;
/**
* 数据源url
*/
@ExcelProperty(value = "数据源url")
private String connUrl;
/**
* 数据源用户名
*/
@ExcelProperty(value = "数据源用户名")
private String username;
/**
* 数据源密码
*/
@ExcelProperty(value = "数据源密码")
private String password;
/**
* 驱动类名
*/
@ExcelProperty(value = "驱动类名")
private String driverClassName;
/**
* 备注
*/
@ExcelProperty(value = "备注")
private String remark;
}

View File

@ -0,0 +1,27 @@
package org.dromara.datasource.jdbc;
import java.io.Serial;
/**
* Database 异常类
*
* @author ixyxj
* @since 2024/5/2
*/
public class DatabaseException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
public DatabaseException(String message) {
super(message);
}
public DatabaseException(Throwable throwable) {
super(throwable);
}
public DatabaseException(String message, Throwable throwable) {
super(message, throwable);
}
}

View File

@ -0,0 +1,223 @@
package org.dromara.datasource.jdbc;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.datasource.jdbc.core.DataType;
import org.dromara.datasource.jdbc.core.InternalFunction;
import org.dromara.datasource.jdbc.core.StorageEngine;
import org.dromara.datasource.utils.ExceptionUtils;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* jdbc基础查询接口
*
* @author xyxj xieyangxuejun@gmail.com
* @since 2024/5/2
*/
public interface JdbcQuery {
String SQL_CURRENT_DB = "select database()";
/**
* 获取JdbcTemplate
*/
JdbcTemplate getJdbc();
/**
* 分页查询
*/
default IPage<Map<String, Object>> queryForPage(IPage<Map<String, Object>> page, String sql) throws DatabaseException {
if (StringUtils.isEmpty(sql)) {
throw ExceptionUtils.create("sql should not empty");
}
JdbcTemplate jdbc = getJdbc();
int total = Optional.ofNullable(jdbc.queryForObject("select count(1) from (" + sql + ") t", Integer.class))
.orElse(0);
page.setTotal(total);
if (total > 0) {
List<Map<String, Object>> maps = jdbc.queryForList(sql + " LIMIT " + page.offset() + StringPool.COMMA + page.getSize());
page.setRecords(maps);
}
return page;
}
/**
* 分页查询
*/
default <T> IPage<T> queryForPage(IPage<T> page, String sql, Class<T> elementType) throws DatabaseException {
if (StringUtils.isEmpty(sql)) {
throw ExceptionUtils.create("sql should not empty");
}
JdbcTemplate jdbc = getJdbc();
int total = Optional.ofNullable(jdbc.queryForObject("select count(1) from (" + sql + ") t", Integer.class))
.orElse(0);
page.setTotal(total);
if (total > 0) {
BeanPropertyRowMapper<T> rowMapper = BeanPropertyRowMapper.newInstance(elementType);
List<T> list = jdbc.query(sql + " LIMIT " + page.offset() + StringPool.COMMA + page.getSize(), rowMapper);
page.setRecords(list);
}
return page;
}
/**
* 测试连接是否成功
*/
default boolean testConnection() {
try {
getJdbc().queryForMap("select 1");
return true;
} catch (Exception ignored) {
// ignored
}
return false;
}
/**
* 查询数据库
*/
default List<String> queryDatabases() throws DatabaseException {
try {
return getJdbc().queryForList("show databases", String.class);
} catch (Exception e) {
throw new DatabaseException(e);
}
}
/**
* 查询表数据, 当前数据库
*/
default List<Map<String, Object>> queryTables(String... tables) throws DatabaseException {
return querySchemaTables(null, tables);
}
/**
* 查询表数据
*/
default List<Map<String, Object>> querySchemaTables(String schema, String... tables) throws DatabaseException {
try {
return getJdbc().queryForList(sqlQueryTable(schema, tables));
} catch (Exception e) {
throw new DatabaseException(e);
}
}
/**
* 分页查询表数据
*/
default IPage<Map<String, Object>> queryTablePage(IPage<Map<String, Object>> page) throws DatabaseException {
try {
return queryForPage(page, sqlQueryTable());
} catch (Exception e) {
throw new DatabaseException(e);
}
}
/**
* 查询表sql
*/
default String sqlQueryTable() {
return sqlQueryTable(null);
}
/**
* 查询表可以指定表名
*/
default String sqlQueryTable(String schema, String... tables) {
throw ExceptionUtils.methodNotImplemented();
}
/**
* 查询表字段
*/
default List<Map<String, Object>> queryTableFields(String schema, String table) throws DatabaseException {
throw ExceptionUtils.methodNotImplemented();
}
/**
* 获取数据类型
*/
default List<DataType> queryDateTypes() throws DatabaseException {
throw ExceptionUtils.methodNotImplemented();
}
/**
* 获取引擎类型
*/
default List<StorageEngine> queryEngines() throws DatabaseException {
throw ExceptionUtils.methodNotImplemented();
}
/**
* 获取数据库内置方法
*/
default List<InternalFunction> queryFunctions() throws DatabaseException {
throw ExceptionUtils.methodNotImplemented();
}
/**
* 获取数据库集群数据
*/
default List<String> queryClusters() throws DatabaseException {
throw ExceptionUtils.methodNotImplemented();
}
/**
* 将map装换为javabean对象
*/
default <T> T mapToBean(Map<String, Object> map, Class<T> clz) {
try {
return mapToBean(map, clz.newInstance());
} catch (InstantiationException | IllegalAccessException ignored) {
return null;
}
}
default <T> T mapToBean(Map<String, Object> map, T bean) {
Field[] fields = bean.getClass().getDeclaredFields();
for (Field field : fields) {
int mod = field.getModifiers();
if (Modifier.isStatic(mod) || Modifier.isFinal(mod)) {
continue;
}
field.setAccessible(true);
if (map.containsKey(field.getName())) {
try {
Object value = map.get(field.getName());
if (value instanceof BigInteger) {
value = ((BigInteger) value).intValue();
}
field.set(bean, value);
} catch (IllegalAccessException ignored) {
}
}
}
return bean;
}
/**
* bean转Map
*/
default <T> Map<String, Object> beanToMap(T bean) {
Map<String, Object> map = new HashMap<>();
Field[] fields = bean.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
try {
map.put(field.getName(), field.get(bean));
} catch (IllegalAccessException ignored) {
}
}
return map;
}
}

View File

@ -0,0 +1,114 @@
package org.dromara.datasource.jdbc;
import org.dromara.datasource.jdbc.core.DbType;
import org.dromara.datasource.utils.ExceptionUtils;
import javax.sql.DataSource;
import java.lang.reflect.Constructor;
/**
* jdbc query 构造器
*
* @author xyxj xieyangxuejun@gmail.com
* @since 2024/5/2
*/
public class JdbcQueryBuilder {
private JdbcQueryBuilder() {
}
// 构造者模式
public static final class Builder {
private boolean ignoreWarnings = true;
private int fetchSize = -1;
private int maxRows = -1;
private int queryTimeout = -1;
private boolean skipResultsProcessing = false;
private boolean skipUndeclaredResults = false;
private boolean resultsMapCaseInsensitive = false;
private DataSource dataSource;
private DbType dbType = DbType.MYSQL;
public Builder setIgnoreWarnings(boolean ignoreWarnings) {
this.ignoreWarnings = ignoreWarnings;
return this;
}
public Builder setFetchSize(int fetchSize) {
this.fetchSize = fetchSize;
return this;
}
public Builder setMaxRows(int maxRows) {
this.maxRows = maxRows;
return this;
}
public Builder setQueryTimeout(int queryTimeout) {
this.queryTimeout = queryTimeout;
return this;
}
public Builder setSkipResultsProcessing(boolean skipResultsProcessing) {
this.skipResultsProcessing = skipResultsProcessing;
return this;
}
public Builder setSkipUndeclaredResults(boolean skipUndeclaredResults) {
this.skipUndeclaredResults = skipUndeclaredResults;
return this;
}
public Builder setResultsMapCaseInsensitive(boolean resultsMapCaseInsensitive) {
this.resultsMapCaseInsensitive = resultsMapCaseInsensitive;
return this;
}
public Builder setDbType(DbType dbType) {
this.dbType = dbType;
return this;
}
/**
* 必须设置Datasource
*/
public Builder setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
return this;
}
public JdbcQuery build() throws DatabaseException {
if (dataSource == null) {
throw ExceptionUtils.create("必须设置DataSource");
}
if (dbType == null) {
throw ExceptionUtils.create("数据类型未空");
}
JdbcQuery jdbcQuery;
try {
Class<?> clz = Class.forName(dbType.getQueryClass());
Constructor<?> constructor = clz.getConstructor(DataSource.class);
jdbcQuery = (JdbcQuery) constructor.newInstance(dataSource);
} catch (Exception e) {
throw ExceptionUtils.create("创建JdbcQuery失败:" + e.getMessage());
}
if (!ignoreWarnings) jdbcQuery.getJdbc().setIgnoreWarnings(false);
if (-1 != fetchSize) jdbcQuery.getJdbc().setFetchSize(fetchSize);
if (-1 != maxRows) jdbcQuery.getJdbc().setMaxRows(maxRows);
if (-1 != queryTimeout) jdbcQuery.getJdbc().setQueryTimeout(queryTimeout);
if (skipResultsProcessing) jdbcQuery.getJdbc().setSkipResultsProcessing(true);
if (skipUndeclaredResults) jdbcQuery.getJdbc().setSkipUndeclaredResults(true);
if (resultsMapCaseInsensitive) jdbcQuery.getJdbc().setResultsMapCaseInsensitive(true);
return jdbcQuery;
}
}
}

View File

@ -0,0 +1,180 @@
package org.dromara.datasource.jdbc;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import com.zaxxer.hikari.HikariDataSource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.datasource.domain.SysDatasource;
import org.dromara.datasource.jdbc.core.DbType;
import org.dromara.datasource.utils.Assert;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.stereotype.Service;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* 动态数据源执行服务
*
* @author xyxj xieyangxuejun@gmail.com
* @since 2024/5/2
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class JdbcService {
// 连接测试sql
private static final String TEST_SQL = "SELECT 1";
/**
* 查询缓存没必要每次都去获取dataSource
*/
private final ConcurrentMap<String, JdbcQuery> jdbcQueryMap = new ConcurrentHashMap<>();
/**
* 动态数据源组件
*/
private final DynamicRoutingDataSource dynamicRoutingDataSource;
/**
* 获取数据源
*/
public DataSource getDataSource(String dataSourceName) {
DataSource dataSource = dynamicRoutingDataSource.getDataSource(dataSourceName);
Assert.notNull(dataSource, "数据源【%s】不存在".formatted(dataSourceName));
Assert.isTrue(testConnection(dataSource), "数据源【%s】不存在".formatted(dataSourceName));
return dataSource;
}
/**
* 测试连接
*/
public boolean testConnection(String url, String username, String password, String driverClassName) {
DataSourceBuilder<HikariDataSource> builder = DataSourceBuilder.create()
.type(HikariDataSource.class)
.driverClassName(driverClassName)
.url(url)
.username(username)
.password(password);
DataSource dataSource = builder.build();
return testConnection(dataSource);
}
/**
* 测试连接
*/
public boolean testConnection(DataSource dataSource) {
try (Connection conn = dataSource.getConnection()) {
try (Statement stmt = conn.createStatement()) {
ResultSet resultSet = stmt.executeQuery(TEST_SQL);
return resultSet.next();
}
} catch (SQLException e) {
throw new ServiceException(e.getMessage());
}
}
/**
* 添加数据源
*/
public void addDataSource(List<SysDatasource> datasourceList) {
for (SysDatasource sysDatasource : datasourceList) {
try {
String dataSourceName = sysDatasource.getDsName();
String url = sysDatasource.getConnUrl();
String username = sysDatasource.getUsername();
String password = sysDatasource.getPassword();
String driverClassName = sysDatasource.getDriverClassName();
addDataSource(dataSourceName, url, username, password, driverClassName);
} catch (Exception ignored) {
// ignore
}
}
}
/**
* 添加数据源
*/
public void addDataSource(String dataSourceName, String url, String username, String password, String driverClassName) {
Assert.isTrue(!dynamicRoutingDataSource.getDataSources().containsKey(dataSourceName), "数据源【%s】不存在".formatted(dataSourceName));
DataSourceBuilder<HikariDataSource> builder = DataSourceBuilder.create()
.type(HikariDataSource.class)
.driverClassName(driverClassName)
.url(url)
.username(username)
.password(password);
DataSource dataSource = builder.build();
dynamicRoutingDataSource.addDataSource(dataSourceName, dataSource);
}
/**
* 移除数据源
*/
public void removeDataSource(String dataSourceName) {
dynamicRoutingDataSource.removeDataSource(dataSourceName);
}
/**
* 修改数据源
*/
public void updateDataSource(String dataSourceName, String url, String username, String password, String driverClassName) {
Assert.isTrue(dynamicRoutingDataSource.getDataSources().containsKey(dataSourceName), "数据源【%s】不存在".formatted(dataSourceName));
addDataSource(dataSourceName, url, username, password, driverClassName);
}
/**
* 查询sql
*/
public <R> R query(String dsName, String dsType, Function<JdbcQuery, R> func) {
try {
DynamicDataSourceContextHolder.push(dsName);
return func.apply(getJdbcQuery(dsName, dsType));
} finally {
DynamicDataSourceContextHolder.clear();
}
}
/**
* 执行没有返回值
*/
public void execute(String dsName, String dsType, Consumer<JdbcQuery> consumer) {
try {
DynamicDataSourceContextHolder.push(dsName);
consumer.accept(getJdbcQuery(dsName, dsType));
} finally {
DynamicDataSourceContextHolder.clear();
}
}
/**
* 获取JdbcQuery
*/
private JdbcQuery getJdbcQuery(String dsName, String dsType) {
DataSource dataSource = getDataSource(dsName);
JdbcQuery jdbcQuery = jdbcQueryMap.get(dsName);
// 判断是否连接
if (Objects.isNull(jdbcQuery) || !jdbcQuery.testConnection()) {
jdbcQuery = new JdbcQueryBuilder.Builder()
.setDbType(DbType.getDbType(dsType))
.setDataSource(dataSource)
.build();
jdbcQueryMap.put(dsName, jdbcQuery);
}
return jdbcQuery;
}
}

View File

@ -0,0 +1,19 @@
package org.dromara.datasource.jdbc.core;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 数据类型
*
* @author xyxj xieyangxuejun@gmail.com
* @since 2024/5/2
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DataType {
private String name;
private boolean signed;
}

View File

@ -0,0 +1,268 @@
package org.dromara.datasource.jdbc.core;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 数据库类型复制自com.baomidou.mybatisplus.core.enums.DbType
*
* @author xyxj xieyangxuejun@gmail.com
* @since 2024/5/2
*/
@Getter
@AllArgsConstructor
public enum DbType {
/**
* MYSQL
*/
MYSQL("mysql", "com.mysql.cj.jdbc.Driver", "org.dromara.datasource.jdbc.query.MysqlJdbcTemplate", "MySql数据库"),
/**
* MARIADB
*/
MARIADB("mariadb", "org.mariadb.jdbc.Driver", "org.dromara.datasource.jdbc.query.MysqlJdbcTemplate", "MariaDB数据库"),
/**
* ORACLE
*/
ORACLE("oracle", "oracle.jdbc.OracleDriver", "", "Oracle11g及以下数据库(高版本推荐使用ORACLE_NEW)"),
/**
* oracle12c new pagination
*/
ORACLE_12C("oracle12c", "oracle.jdbc.OracleDriver", "", "Oracle12c+数据库"),
/**
* DB2
*/
DB2("db2", "com.ibm.db2.jcc.DB2Driver", "", "DB2数据库"),
/**
* H2
*/
H2("h2", "org.h2.Driver", "", "H2数据库"),
/**
* HSQL
*/
HSQL("hsql", "org.hsqldb.jdbc.JDBCDriver", "", "HSQL数据库"),
/**
* SQLITE
*/
SQLITE("sqlite", "org.sqlite.JDBC", "", "SQLite数据库"),
/**
* POSTGRE
*/
POSTGRE_SQL("postgresql", "org.postgresql.Driver", "", "Postgre数据库"),
/**
* SQLSERVER2005
*/
SQL_SERVER2005("sqlserver2005", "com.microsoft.sqlserver.jdbc.SQLServerDriver", "", "SQLServer2005数据库"),
/**
* SQLSERVER
*/
SQL_SERVER("sqlserver", "com.microsoft.sqlserver.jdbc.SQLServerDriver", "", "SQLServer数据库"),
/**
* DM
*/
DM("dm", "dm.jdbc.driver.DmDriver", "", "达梦数据库"),
/**
* xugu
*/
XU_GU("xugu", "com.xugu.cloudjdbc.Driver", "", "虚谷数据库"),
/**
* Kingbase
*/
KINGBASE_ES("kingbasees", "cn.kingbase.es.mapreduce.jdbc.KingbaseDriver", "", "人大金仓数据库"),
/**
* Phoenix
*/
PHOENIX("phoenix", "org.apache.phoenix.jdbc.PhoenixDriver", "", "Phoenix HBase数据库"),
/**
* Gauss
*/
GAUSS("zenith", "com.huawei.gauss.jdbc.ZenithDriver", "", "Gauss 数据库"),
/**
* ClickHouse
*/
CLICK_HOUSE("clickhouse", "ru.yandex.clickhouse.ClickHouseDriver", "org.dromara.datasource.jdbc.query.ClickHouseJdbcTemplate", "clickhouse 数据库"),
/**
* GBase
*/
GBASE("gbase", "com.gbase.jdbc.Driver", "", "南大通用(华库)数据库"),
/**
* GBase-8s
*/
GBASE_8S("gbase-8s", "com.gbase.jdbc.Driver", "", "南大通用数据库 GBase 8s"),
/**
* use {@link #GBASE_8S}
*
* @deprecated 2022-05-30
*/
@Deprecated
GBASEDBT("gbasedbt", "com.gbasedbt.jdbc.Driver", "", "南大通用数据库"),
/**
* use {@link #GBASE_8S}
*
* @deprecated 2022-05-30
*/
@Deprecated
GBASE_INFORMIX("gbase 8s", "com.gbase.jdbc.Driver", "", "南大通用数据库 GBase 8s"),
/**
* GBase8sPG
*/
GBASE8S_PG("gbase8s-pg", "com.gbasedbt.jdbc.Driver", "", "南大通用数据库 GBase 8s兼容pg"),
/**
* GBase8c
*/
GBASE_8C("gbase8c", "com.gbase.jdbc.Driver", "", "南大通用数据库 GBase 8c"),
/**
* Sinodb
*/
SINODB("sinodb", "com.sinodb.jdbc.Driver", "", "星瑞格数据库"),
/**
* Oscar
*/
OSCAR("oscar", "com.oscar.Driver", "", "神通数据库"),
/**
* Sybase
*/
SYBASE("sybase", "com.sybase.jdbc4.jdbc.SybDriver", "", "Sybase ASE 数据库"),
/**
* OceanBase
*/
OCEAN_BASE("oceanbase", "com.aliyun.oceanbase.jdbc.OceanBaseDriver", "", "OceanBase 数据库"),
/**
* Firebird
*/
FIREBIRD("Firebird", "org.firebirdsql.jdbc.FBDriver", "", "Firebird 数据库"),
/**
* HighGo
*/
HIGH_GO("highgo", "com.highgo.jdbc.Driver", "", "瀚高数据库"),
/**
* CUBRID
*/
CUBRID("cubrid", "cubrid.jdbc.driver.CUBRIDDriver", "", "CUBRID数据库"),
/**
* SUNDB
*/
SUNDB("sundb", "com.unicom.sundb.jdbc.sundbjdbcdriver", "", "SUNDB数据库"),
/**
* Hana
*/
SAP_HANA("hana", "com.sap.db.jdbc.Driver", "", "SAP_HANA数据库"),
/**
* Impala
*/
IMPALA("impala", "org.apache.hive.jdbc.HiveDriver", "", "impala数据库"),
/**
* Vertica
*/
VERTICA("vertica", "com.vertica.jdbc.Driver", "", "vertica数据库"),
/**
* xcloud
*/
XCloud("xcloud", "com.xcloud.jdbc.driver.XCloudDriver", "", "行云数据库"),
/**
* redshift
*/
REDSHIFT("redshift", "com.amazon.redshift.jdbc.Driver", "", "亚马逊redshift数据库"),
/**
* openGauss
*/
OPENGAUSS("openGauss", "org.opengauss.Driver", "", "华为 opengauss 数据库"),
/**
* TDengine
*/
TDENGINE("TDengine", "com.taosdata.jdbc.TSDBDriver", "", "TDengine数据库"),
/**
* Informix
*/
INFORMIX("informix", "com.informix.jdbc.IfxDriver", "", "Informix数据库"),
/**
* uxdb
*/
UXDB("uxdb", "com.uxun.jdbc.Driver", "", "优炫数据库"),
/**
* lealone
*/
LEALONE("lealone", "org.lealone.Driver", "", "Lealone数据库"),
/**
* trino
*/
TRINO("trino", "io.trino.jdbc.TrinoDriver", "", "Trino数据库"),
/**
* presto
*/
PRESTO("presto", "com.facebook.presto.jdbc.PrestoDriver", "", "Presto数据库"),
/**
* UNKNOWN DB
*/
OTHER("other", "", "", "其他数据库");
/**
* 数据库名称
*/
private final String db;
/**
* 驱动类
*/
private final String driverClass;
/**
* 查询类
*/
private final String queryClass;
/**
* 描述
*/
private final String desc;
/**
* 获取数据库类型
*
* @param dbType 数据库类型字符串
*/
public static DbType getDbType(String dbType) {
for (DbType type : DbType.values()) {
if (type.db.equalsIgnoreCase(dbType)) {
return type;
}
}
return OTHER;
}
public boolean mysqlSameType() {
return this == DbType.MYSQL
|| this == DbType.MARIADB
|| this == DbType.GBASE
|| this == DbType.OSCAR
|| this == DbType.XU_GU
|| this == DbType.CLICK_HOUSE
|| this == DbType.OCEAN_BASE
|| this == DbType.CUBRID
|| this == DbType.SUNDB;
}
public boolean oracleSameType() {
return this == DbType.ORACLE
|| this == DbType.DM
|| this == DbType.GAUSS;
}
public boolean postgresqlSameType() {
return this == DbType.POSTGRE_SQL
|| this == DbType.H2
|| this == DbType.LEALONE
|| this == DbType.SQLITE
|| this == DbType.HSQL
|| this == DbType.KINGBASE_ES
|| this == DbType.PHOENIX
|| this == DbType.SAP_HANA
|| this == DbType.IMPALA
|| this == DbType.HIGH_GO
|| this == DbType.VERTICA
|| this == DbType.REDSHIFT
|| this == DbType.OPENGAUSS
|| this == DbType.TDENGINE
|| this == DbType.UXDB
|| this == DbType.GBASE8S_PG
|| this == DbType.GBASE_8C;
}
}

View File

@ -0,0 +1,22 @@
package org.dromara.datasource.jdbc.core;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* InternalFunctions
* 内置函数
*
* @author xyxj xieyangxuejun@gmail.com
* @since 2024/5/2
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class InternalFunction {
private String name;
private String category;
private String description;
private String syntax;
}

View File

@ -0,0 +1,25 @@
package org.dromara.datasource.jdbc.core;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* StorageEngine
* 储存引擎
*
* @author xyxj xieyangxuejun@gmail.com
* @since 2024/5/2
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class StorageEngine {
private String name;
private int isDefault;
private String comment;
private List<Map<String, Object>> params;
}

View File

@ -0,0 +1,110 @@
package org.dromara.datasource.jdbc.query;
import com.clickhouse.data.ClickHouseDataType;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.datasource.jdbc.DatabaseException;
import org.dromara.datasource.jdbc.JdbcQuery;
import org.dromara.datasource.jdbc.core.DataType;
import org.dromara.datasource.jdbc.core.InternalFunction;
import org.dromara.datasource.jdbc.core.StorageEngine;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* ClickHouse 查询
*
* @author xyxj xieyangxuejun@gmail.com
* @since 2024/5/2
*/
public final class ClickHouseJdbcTemplate extends JdbcTemplate implements JdbcQuery {
public ClickHouseJdbcTemplate(DataSource dataSource) {
super(dataSource);
}
@Override
public ClickHouseJdbcTemplate getJdbc() {
return this;
}
@Override
public String sqlQueryTable(String schema, String... tables) {
StringBuilder sb = new StringBuilder()
.append("select name as tableName,\n")
.append("database as tableSchema\n")
.append("from system.tables\n");
if (StringUtils.isEmpty(schema)) {
sb.append("where database=(").append(SQL_CURRENT_DB).append(")\n");
} else {
sb.append("where database='").append(schema).append("'\n");
}
if (tables != null && tables.length != 0) {
sb.append(" and name in ('").append(String.join("','", tables)).append("')\n");
}
sb.append(" and not startsWith(name, '.')\n")
.append("order by database, name");
return sb.toString();
}
@Override
public List<Map<String, Object>> queryTableFields(String schema, String table) throws DatabaseException {
try {
StringBuilder sb = new StringBuilder()
.append("select distinct c.table as tableName,\n")
.append("c.name as columnName,\n")
.append("c.type as columnType,\n")
.append("c.type as dataType,\n")
.append("int32(c.position) as sort\n")
.append("from system.columns c,\n")
.append("system.tables t\n")
.append("where t.database = c.database and t.name = c.table\n");
if (StringUtils.isEmpty(schema)) {
sb.append(" and t.database=(" + SQL_CURRENT_DB + ")\n");
} else {
sb.append(" and t.database='").append(schema).append("'\n");
}
if (StringUtils.isNotEmpty(table)) {
sb.append(" and t.name='").append(table).append("'");
}
return queryForList(sb.toString());
} catch (Exception e) {
throw new DatabaseException(e);
}
}
@Override
public List<DataType> queryDateTypes() throws DatabaseException {
return Arrays.stream(ClickHouseDataType.values()).map(type -> new DataType(type.name(), type.isSigned())).collect(Collectors.toList());
}
@Override
public List<StorageEngine> queryEngines() throws DatabaseException {
String sql = "select name, comment from system.storage_engines";
return getJdbc().query(sql, BeanPropertyRowMapper.newInstance(StorageEngine.class));
}
@Override
public List<InternalFunction> queryFunctions() throws DatabaseException {
// 将alias to 合并
// select arrayJoin(if(f.alias_to <> '', [f.name,f.alias_to], [f.name])) as name, f.* from system.functions f where f.alias_to <> '';
String sql = String.format("select arrayJoin(if(f.alias_to <> '', [f.name,f.alias_to], [f.name])) as name, f.*\n" +
" from system.functions f\n" +
" where name <> %s", "''");
return getJdbc().queryForList(sql).stream().map(v -> {
InternalFunction functions = new InternalFunction();
functions.setName(v.getOrDefault("name", "").toString());
return functions;
}).toList();
}
@Override
public List<String> queryClusters() throws DatabaseException {
String sql = "select distinct cluster from system.clusters";
return getJdbc().queryForList(sql, String.class);
}
}

View File

@ -0,0 +1,110 @@
package org.dromara.datasource.jdbc.query;
import com.mysql.cj.MysqlType;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.datasource.jdbc.DatabaseException;
import org.dromara.datasource.jdbc.JdbcQuery;
import org.dromara.datasource.jdbc.core.DataType;
import org.dromara.datasource.jdbc.core.StorageEngine;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* mysql 查询类
*
* @author xyxj xieyangxuejun@gmail.com
* @since 2024/5/2
*/
public class MysqlJdbcTemplate extends JdbcTemplate implements JdbcQuery {
public MysqlJdbcTemplate(DataSource dataSource) {
super(dataSource);
}
@Override
public MysqlJdbcTemplate getJdbc() {
return this;
}
@Override
public String sqlQueryTable(String schema, String... tables) {
StringBuilder sb = new StringBuilder()
.append("select table_name as tableName,\n")
.append("table_comment as tableComment,\n")
.append("create_time as createTime,\n")
.append("update_time as updateTime,\n")
.append("table_schema as tableSchema\n")
.append("from information_schema.tables\n")
.append("where table_type in ('BASE TABLE', 'SYSTEM VIEW')");
if (StringUtils.isEmpty(schema)) {
sb.append(" and table_schema=(" + SQL_CURRENT_DB + ")\n");
} else {
sb.append(" and table_schema='").append(schema).append("'\n");
}
if (tables != null && tables.length != 0) {
sb.append(" and table_name in ('").append(String.join("','", tables)).append("')\n");
}
sb.append("order by 2, 1");
return sb.toString();
}
@Override
public List<Map<String, Object>> queryTableFields(String schema, String table) throws DatabaseException {
try {
StringBuilder sb = new StringBuilder()
.append("select distinct c.table_name as tableName,\n")
.append("c.column_name as columnName,\n")
.append("c.column_type as columnType,\n")
.append("c.column_comment as columnComment,\n")
.append("c.data_type as dataType,\n")
.append("(case when (c.is_nullable = 'no' && column_key != 'PRI') then '1' else '0' end) as isRequired,\n")
.append("(case when column_key = 'PRI' then '1' else '0' end) as isPk,\n")
.append("(case when extra = 'auto_increment' then '1' else '0' end) as isIncrement,\n")
.append("c.character_maximum_length as dataLength,\n")
.append("c.numeric_precision as dataPrecision,\n")
.append("c.numeric_scale as dataScale,\n")
.append("c.ordinal_position as sort\n")
.append("from information_schema.columns c,\n")
.append("information_schema.tables t\n")
.append("where t.table_name = c.table_name\n")
.append(" and t.table_type in ('BASE TABLE', 'SYSTEM VIEW')\n");
if (StringUtils.isEmpty(schema)) {
sb.append(" and t.table_schema=(" + SQL_CURRENT_DB + ")\n");
} else {
sb.append(" and t.table_schema='").append(schema).append("'\n");
}
if (StringUtils.isNotEmpty(table)) {
sb.append(" and t.table_name='").append(table).append("'");
}
return queryForList(sb.toString());
} catch (Exception e) {
throw new DatabaseException(e);
}
}
@Override
public List<DataType> queryDateTypes() throws DatabaseException {
return Arrays.stream(MysqlType.values()).map(type -> new DataType(type.getName(), type.getCreateParams().contains("UNSIGNED"))).collect(Collectors.toList());
}
@Override
public List<StorageEngine> queryEngines() throws DatabaseException {
String sql = String.format("select e.engine as name,\n" +
" if(e.SUPPORT='DEFAULT', 1, 0) as isDefault,\n" +
" e.COMMENT as comment\n" +
" from information_schema.engines e\n" +
" where e.support in ('%s', '%s')", "DEFAULT", "YES");
return getJdbc().queryForList(sql).stream()
.map(v -> new StorageEngine(
v.getOrDefault("name", "").toString(),
Integer.parseInt(v.get("isDefault").toString()),
v.getOrDefault("comment", "").toString(),
null)
).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,15 @@
package org.dromara.datasource.mapper;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.datasource.domain.SysDatasource;
import org.dromara.datasource.domain.vo.SysDatasourceVo;
/**
* 动态数据源Mapper接口
*
* @author ixyxj
* @date 2024-05-02
*/
public interface SysDatasourceMapper extends BaseMapperPlus<SysDatasource, SysDatasourceVo> {
}

View File

@ -0,0 +1,70 @@
package org.dromara.datasource.service;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.datasource.domain.bo.DsFieldQueryBo;
import org.dromara.datasource.domain.bo.SysDatasourceBo;
import org.dromara.datasource.domain.vo.SysDatasourceVo;
import org.dromara.datasource.jdbc.JdbcService;
import java.util.Collection;
import java.util.List;
/**
* 动态数据源Service接口
*
* @author ixyxj
* @date 2024-05-02
*/
public interface ISysDatasourceService {
/**
* 查询动态数据源
*/
SysDatasourceVo queryById(Long id);
/**
* 通过名称查询
*/
SysDatasourceVo queryByName(String name);
/**
* 查询动态数据源列表
*/
TableDataInfo<SysDatasourceVo> queryPageList(SysDatasourceBo bo, PageQuery pageQuery);
/**
* 查询动态数据源列表
*/
List<SysDatasourceVo> queryList(SysDatasourceBo bo);
/**
* 新增动态数据源
*/
Boolean insertByBo(SysDatasourceBo bo);
/**
* 修改动态数据源
*/
Boolean updateByBo(SysDatasourceBo bo);
/**
* 校验并批量删除动态数据源信息
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 获取JdbcService
*/
JdbcService getJdbcService();
/**
* 测试连接
*/
Boolean testConnByBo(SysDatasourceBo bo);
/**
* 查询字段数据
*/
Object queryFieldData(DsFieldQueryBo bo);
}

View File

@ -0,0 +1,160 @@
package org.dromara.datasource.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.utils.Lists;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.datasource.domain.SysDatasource;
import org.dromara.datasource.domain.bo.DsFieldQueryBo;
import org.dromara.datasource.domain.bo.SysDatasourceBo;
import org.dromara.datasource.domain.vo.SysDatasourceVo;
import org.dromara.datasource.jdbc.JdbcService;
import org.dromara.datasource.mapper.SysDatasourceMapper;
import org.dromara.datasource.service.ISysDatasourceService;
import org.dromara.datasource.utils.Assert;
import org.dromara.datasource.utils.SqlUtils;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 动态数据源Service业务层处理
*
* @author ixyxj
* @date 2024-05-02
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class SysDatasourceServiceImpl implements ISysDatasourceService {
private final JdbcService jdbcService;
private final SysDatasourceMapper baseMapper;
@PostConstruct
public void init() {
List<SysDatasource> datasourceList = baseMapper.selectList();
jdbcService.addDataSource(datasourceList);
log.info("添加动态数据源:{}", datasourceList.stream().map(SysDatasource::getDsName).collect(Collectors.joining(",")));
}
/**
* 查询动态数据源
*/
@Override
public SysDatasourceVo queryById(Long id){
return baseMapper.selectVoById(id);
}
@Override
public SysDatasourceVo queryByName(String name) {
return baseMapper.selectVoOne(Wrappers.<SysDatasource>lambdaQuery().eq(SysDatasource::getDsName, name));
}
/**
* 查询动态数据源列表
*/
@Override
public TableDataInfo<SysDatasourceVo> queryPageList(SysDatasourceBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<SysDatasource> lqw = buildQueryWrapper(bo);
Page<SysDatasourceVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询动态数据源列表
*/
@Override
public List<SysDatasourceVo> queryList(SysDatasourceBo bo) {
LambdaQueryWrapper<SysDatasource> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<SysDatasource> buildQueryWrapper(SysDatasourceBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<SysDatasource> lqw = Wrappers.lambdaQuery();
lqw.like(StringUtils.isNotBlank(bo.getDsName()), SysDatasource::getDsName, bo.getDsName());
lqw.eq(StringUtils.isNotBlank(bo.getDsType()), SysDatasource::getDsType, bo.getDsType());
lqw.like(StringUtils.isNotBlank(bo.getUsername()), SysDatasource::getUsername, bo.getUsername());
lqw.eq(StringUtils.isNotBlank(bo.getDriverClassName()), SysDatasource::getDriverClassName, bo.getDriverClassName());
return lqw;
}
/**
* 新增动态数据源
*/
@Override
public Boolean insertByBo(SysDatasourceBo bo) {
SysDatasource add = MapstructUtils.convert(bo, SysDatasource.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
jdbcService.addDataSource(bo.getDsName(), bo.getConnUrl(), bo.getUsername(), bo.getPassword(), bo.getDriverClassName());
}
return flag;
}
/**
* 修改动态数据源
*/
@Override
public Boolean updateByBo(SysDatasourceBo bo) {
SysDatasource update = MapstructUtils.convert(bo, SysDatasource.class);
validEntityBeforeSave(update);
jdbcService.updateDataSource(bo.getDsName(), bo.getConnUrl(), bo.getUsername(), bo.getPassword(), bo.getDriverClassName());
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(SysDatasource entity){
//TODO 做一些数据校验,如唯一约束
}
/**
* 批量删除动态数据源
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
}
baseMapper.selectBatchIds(ids, sysDatasource -> jdbcService.removeDataSource(sysDatasource.getResultObject().getDsName()));
return baseMapper.deleteBatchIds(ids) > 0;
}
@Override
public JdbcService getJdbcService() {
return jdbcService;
}
@Override
public Boolean testConnByBo(SysDatasourceBo bo) {
return jdbcService.testConnection(bo.getConnUrl(), bo.getUsername(), bo.getPassword(), bo.getDriverClassName());
}
@Override
public Object queryFieldData(DsFieldQueryBo bo) {
SysDatasourceVo datasource = queryByName(bo.getDsName());
Assert.notNull(datasource, "数据库【%s】不存在".formatted(bo.getDsName()));
if (datasource != null) {
return jdbcService.query(datasource.getDsName(), datasource.getDsType(), jdbcQuery -> {
String sql = SqlUtils.buildSqlString(bo, "`" + bo.getSchema() + "`.`" + bo.getTable() + "`");
return SqlUtils.queryByOffsets(jdbcQuery.getJdbc(), sql, bo.getOffsets());
});
}
return Lists.newArrayList();
}
}

View File

@ -0,0 +1,132 @@
package org.dromara.datasource.utils;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 断言类
*
* @author miemie
* @since 2018-07-24
*/
public final class Assert {
private Assert() {
}
/**
* 断言这个 boolean true
* <p> false 则抛出异常</p>
*
* @param expression boolean
* @param message 消息
*/
public static void isTrue(boolean expression, String message, Object... params) {
if (!expression) {
throw ExceptionUtils.create(message, params);
}
}
/**
* 断言这个 boolean false
* <p> true 则抛出异常</p>
*
* @param expression boolean
* @param message 消息
*/
public static void isFalse(boolean expression, String message, Object... params) {
isTrue(!expression, message, params);
}
/**
* 断言这个 object null
* <p>不为 null 则抛异常</p>
*
* @param object 对象
* @param message 消息
*/
public static void isNull(Object object, String message, Object... params) {
isTrue(object == null, message, params);
}
/**
* 断言这个 object 不为 null
* <p> null 则抛异常</p>
*
* @param object 对象
* @param message 消息
*/
public static void notNull(Object object, String message, Object... params) {
isTrue(object != null, message, params);
}
/**
* 断言这个 map empty
* <p> empty 则抛异常</p>
*
* @param map 集合
* @param message 消息
*/
public static void isEmpty(Map<?, ?> map, String message, Object... params) {
isTrue(CollUtil.isEmpty(map), message, params);
}
/**
* 断言这个 collection empty
* <p> empty 则抛异常</p>
*
* @param collection 集合
* @param message 消息
*/
public static void isEmpty(Collection<?> collection, String message, Object... params) {
isTrue(CollUtil.isEmpty(collection), message, params);
}
/**
* 断言这个 value 不为 empty
* <p> empty 则抛异常</p>
*
* @param value 字符串
* @param message 消息
*/
public static void notEmpty(String value, String message, Object... params) {
isTrue(StrUtil.isNotBlank(value), message, params);
}
/**
* 断言这个 collection 不为 empty
* <p> empty 则抛异常</p>
*
* @param collection 集合
* @param message 消息
*/
public static void notEmpty(Collection<?> collection, String message, Object... params) {
isTrue(CollUtil.isNotEmpty(collection), message, params);
}
/**
* 断言这个 map 不为 empty
* <p> empty 则抛异常</p>
*
* @param map 集合
* @param message 消息
*/
public static void notEmpty(Map<?, ?> map, String message, Object... params) {
isTrue(CollUtil.isNotEmpty(map), message, params);
}
/**
* 断言这个 数组 不为 empty
* <p> empty 则抛异常</p>
*
* @param array 数组
* @param message 消息
*/
public static void notEmpty(Object[] array, String message, Object... params) {
isTrue(CollUtil.isNotEmpty(List.of(array)), message, params);
}
}

View File

@ -0,0 +1,60 @@
package org.dromara.datasource.utils;
import org.apache.ibatis.datasource.DataSourceException;
/**
* ExceptionUtils
* 异常工具类
*
* @author xyxj xieyangxuejun@gmail.com
* @since 2024/5/2
*/
public final class ExceptionUtils {
private ExceptionUtils() {
}
/**
* 返回一个新的异常统一构建方便统一处理
*
* @param msg 消息
* @param t 异常信息
* @return 返回异常
*/
public static DataSourceException create(String msg, Throwable t, Object... params) {
return new DataSourceException(String.format(msg, params), t);
}
/**
* 重载的方法
*
* @param msg 消息
* @return 返回异常
*/
public static DataSourceException create(String msg, Object... params) {
return new DataSourceException(String.format(msg, params));
}
/**
* 重载的方法
*
* @param t 异常
* @return 返回异常
*/
public static DataSourceException create(Throwable t) {
return new DataSourceException(t);
}
public static void throwOr(boolean condition, String msg, Object... params) throws DataSourceException {
if (condition) {
throw create(msg, params);
}
}
/**
* 方法为实现异常
*/
public static DataSourceException methodNotImplemented() {
return create("method not implemented");
}
}

View File

@ -0,0 +1,89 @@
package org.dromara.datasource.utils;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.db.sql.*;
import org.dromara.datasource.domain.bo.FieldQueryBo;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.lang.NonNull;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* sql工具类
*
* @author xyxj xieyangxuejun@gmail.com
* @since 2024/5/2
*/
public class SqlUtils {
private SqlUtils() {
}
/**
* 获取拼接sql
*/
public static String buildSqlString(FieldQueryBo bo, String table) {
SqlBuilder sqlBuilder = SqlBuilder.create();
Collection<String> columnList = bo.getColumns();
String[] columns = StrUtil.wrapAllWithPair(columnList.contains("*") ? "" : "`", columnList.toArray(new String[0]));
sqlBuilder.select(columns).from(table);
// 检索条件
List<FieldQueryBo.FieldCondition> fieldConditions = bo.getFieldConditions();
if (CollUtil.isNotEmpty(fieldConditions)) {
sqlBuilder.where(fieldConditions.stream().filter(fc -> fc.getValue() != null && StrUtil.isNotEmpty(fc.getValue().toString())).map(fc -> {
// value is wrapped
Object value = fc.getValue();
if (!StrUtil.isWrap(value.toString(), "'")) {
value = StrUtil.wrap(value.toString(), "'");
}
Condition condition = new Condition(fc.getField(), fc.getOperator(), value);
condition.setPlaceHolder(false);
condition.setSecondValue(fc.getSecondValue());
if (StrUtil.isNotEmpty(fc.getLinkOperator())) {
condition.setLinkOperator(LogicalOperator.valueOf(fc.getLinkOperator().toUpperCase()));
}
return condition;
}).toArray(Condition[]::new));
}
// 聚合
if (CollUtil.isNotEmpty(bo.getGroupColumns())) {
sqlBuilder.groupBy(bo.getGroupColumns().toArray(new String[0]));
}
// 排序
List<FieldQueryBo.FieldOrder> fieldOrders = bo.getFieldOrders();
if (CollUtil.isNotEmpty(fieldOrders)) {
sqlBuilder.orderBy(fieldOrders.stream().map(fieldOrder -> {
Order order = new Order();
order.setField(fieldOrder.getField());
order.setDirection(Direction.fromString(fieldOrder.getDirection()));
return order;
}).toArray(Order[]::new));
}
// 当有offset的时候limit就不需要
if (CollUtil.isEmpty(bo.getOffsets()) && bo.getLimit() != null) {
sqlBuilder.append(" LIMIT " + bo.getLimit());
}
return sqlBuilder.build();
}
/**
* 根据offsets查询数据
*
* @param jdbc jdbc template
* @param sql sql string
* @param offsets offsets
* @return list
*/
public static List<Map<String, Object>> queryByOffsets(@NonNull JdbcTemplate jdbc, @NonNull String sql, List<Integer> offsets) {
if (CollUtil.isEmpty(offsets)) {
return jdbc.queryForList(sql);
}
// 如果有offsets参数
return offsets.parallelStream().map(offset -> {
String append = " OFFSET " + (offset - 1) + " LIMIT 1";
return jdbc.queryForMap(sql + append);
}).toList();
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.datasource.mapper.SysDatasourceMapper">
</mapper>

View File

@ -292,6 +292,8 @@ insert into sys_menu values('118', '文件管理', '1', '10', 'oss',
insert into sys_menu values('120', '任务调度中心', '2', '5', 'powerjob', 'monitor/powerjob/index', '', 1, 0, 'C', '0', '0', 'monitor:powerjob:list', 'job', 103, 1, sysdate(), null, null, 'PowerJob控制台菜单');
-- retry server控制台
insert into sys_menu values('130', 'EasyRetry控制台', '2', '6', 'easyretry', 'monitor/easyretry/index', '', 1, 0, 'C', '0', '0', 'monitor:easyretry:list', 'job', 103, 1, sysdate(), null, null, 'EasyRetry控制台菜单');
-- 动态数据源管理
insert into sys_menu values('124', '数据源管理', '1', '12', 'datasource', 'system/datasource/index', '', 1, 0, 'C', '0', '0', 'system:datasource:list', 'redis', 103, 1, sysdate(), null, null, '数据源管理菜单');
-- 三级菜单
insert into sys_menu values('500', '操作日志', '108', '1', 'operlog', 'monitor/operlog/index', '', 1, 0, 'C', '0', '0', 'monitor:operlog:list', 'form', 103, 1, sysdate(), null, null, '操作日志菜单');
@ -404,6 +406,12 @@ insert into sys_menu values('1508', '测试树表新增', '1506', '2', '#',
insert into sys_menu values('1509', '测试树表修改', '1506', '3', '#', '', '', 1, 0, 'F', '0', '0', 'demo:tree:edit', '#', 103, 1, sysdate(), null, null, '');
insert into sys_menu values('1510', '测试树表删除', '1506', '4', '#', '', '', 1, 0, 'F', '0', '0', 'demo:tree:remove', '#', 103, 1, sysdate(), null, null, '');
insert into sys_menu values('1511', '测试树表导出', '1506', '5', '#', '', '', 1, 0, 'F', '0', '0', 'demo:tree:export', '#', 103, 1, sysdate(), null, null, '');
-- 动态数据源管理按钮
insert into sys_menu values('1700', '动态数据源查询', '124', '1', '#', '', '', 1, 0, 'F', '0', '0', 'system:datasource:query', '#', 103, 1, sysdate(), null, null, '');
insert into sys_menu values('1701', '动态数据源新增', '124', '2', '#', '', '', 1, 0, 'F', '0', '0', 'system:datasource:add', '#', 103, 1, sysdate(), null, null, '');
insert into sys_menu values('1702', '动态数据源修改', '124', '3', '#', '', '', 1, 0, 'F', '0', '0', 'system:datasource:edit', '#', 103, 1, sysdate(), null, null, '');
insert into sys_menu values('1703', '动态数据源删除', '124', '4', '#', '', '', 1, 0, 'F', '0', '0', 'system:datasource:remove', '#', 103, 1, sysdate(), null, null, '');
insert into sys_menu values('1704', '动态数据源导出', '124', '5', '#', '', '', 1, 0, 'F', '0', '0', 'system:datasource:export', '#', 103, 1, sysdate(), null, null, '');
-- ----------------------------
-- 6、用户和角色关联表 用户N-1角色
@ -936,3 +944,27 @@ INSERT INTO test_tree VALUES (10, '000000', 7, 108, 3, '子节点66', 0, 103, sy
INSERT INTO test_tree VALUES (11, '000000', 7, 108, 3, '子节点77', 0, 103, sysdate(), 1, NULL, NULL, 0);
INSERT INTO test_tree VALUES (12, '000000', 10, 108, 3, '子节点88', 0, 103, sysdate(), 1, NULL, NULL, 0);
INSERT INTO test_tree VALUES (13, '000000', 10, 108, 3, '子节点99', 0, 103, sysdate(), 1, NULL, NULL, 0);
-- ------------------------
-- 动态数据源表
-- ------------------------
drop table if exists sys_datasource;
create table sys_datasource
(
id bigint(20) not null auto_increment comment '主键',
tenant_id varchar(20) default '000000' comment '租户编号',
ds_type varchar(20) comment '数据源类型',
ds_name varchar(255) comment '数据源名称',
conn_url varchar(1000) comment '数据源url',
username varchar(50) comment '数据源用户名',
password varchar(50) comment '数据源密码',
driver_class_name varchar(50) comment '驱动类名',
del_flag char(1) default '0' comment '删除标志0代表存在 2代表删除',
create_dept bigint(20) default null comment '创建部门',
create_by bigint(20) default null comment '创建者',
create_time datetime default null comment '创建时间',
update_by bigint(20) default null comment '更新者',
update_time datetime default null comment '更新时间',
remark varchar(500) default null comment '备注',
primary key (id, ds_name)
) engine = innodb comment ='动态数据源';