diff --git a/pom.xml b/pom.xml index 5b1745f2d..62abb9683 100644 --- a/pom.xml +++ b/pom.xml @@ -389,6 +389,13 @@ ${revision} + + + org.dromara + ruoyi-datasource + ${revision} + + diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml index 155f594c6..350a6a15a 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -81,6 +81,12 @@ ruoyi-workflow + + + org.dromara + ruoyi-datasource + + de.codecentric spring-boot-admin-starter-client diff --git a/ruoyi-modules/pom.xml b/ruoyi-modules/pom.xml index daff497a0..8e0207cac 100644 --- a/ruoyi-modules/pom.xml +++ b/ruoyi-modules/pom.xml @@ -15,6 +15,7 @@ ruoyi-job ruoyi-system ruoyi-workflow + ruoyi-datasource ruoyi-modules diff --git a/ruoyi-modules/ruoyi-datasource/pom.xml b/ruoyi-modules/ruoyi-datasource/pom.xml new file mode 100644 index 000000000..a7e7654b4 --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/pom.xml @@ -0,0 +1,94 @@ + + + 4.0.0 + + org.dromara + ruoyi-modules + ${revision} + ../pom.xml + + + jar + ruoyi-datasource + + + 0.6.0 + + + + 数据源模块 + + + + + + org.dromara + ruoyi-common-core + + + + org.dromara + ruoyi-common-idempotent + + + + + org.dromara + ruoyi-common-mybatis + + + + org.dromara + ruoyi-common-log + + + + org.dromara + ruoyi-common-excel + + + + org.dromara + ruoyi-common-web + + + + org.dromara + ruoyi-common-tenant + + + + com.mysql + mysql-connector-j + + + + com.oracle.database.jdbc + ojdbc8 + + + + org.postgresql + postgresql + + + + com.microsoft.sqlserver + mssql-jdbc + + + + cn.hutool + hutool-db + + + + com.clickhouse + clickhouse-jdbc + ${clickhouse.version} + + + + diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/controller/SysDatasourceController.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/controller/SysDatasourceController.java new file mode 100644 index 000000000..c5e0557ca --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/controller/SysDatasourceController.java @@ -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 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 list = sysDatasourceService.queryList(bo); + ExcelUtil.exportExcel(list, "动态数据源", SysDatasourceVo.class, response); + } + + /** + * 获取动态数据源详细信息 + * + * @param id 主键 + */ + @SaCheckPermission("system:datasource:query") + @GetMapping("/{id}") + public R 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 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 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 remove(@NotEmpty(message = "主键不能为空") + @PathVariable Long[] ids) { + return toAjax(sysDatasourceService.deleteWithValidByIds(List.of(ids), true)); + } + + /** + * 获取数据源类型 + */ + @SaCheckPermission("system:datasource:query") + @GetMapping("/types") + public R types() { + return R.ok(DbType.values()); + } + + /** + * 获取数据库列表 + */ + @SaCheckPermission("system:datasource:query") + @PostMapping("/databases") + public R> databases(@Validated @RequestBody DsQueryBo bo) { + List query = sysDatasourceService.getJdbcService().query(bo.getDsName(), bo.getDsType(), JdbcQuery::queryDatabases); + return R.ok(query); + } + + /** + * 获取表列表 + */ + @SaCheckPermission("system:datasource:query") + @PostMapping("/tables") + public R>> tables(@Validated @RequestBody DsQueryBo bo) { + List> 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>> columns(@Validated @RequestBody DsQueryBo bo) { + List> 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 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>> 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 testConn(@Validated @RequestBody SysDatasourceBo bo) { + return R.ok(sysDatasourceService.testConnByBo(bo)); + } + + /** + * 查询字段数据 + */ + @SaCheckPermission("system:datasource:query") + @PostMapping("/fieldData") + public R queryFieldData(@Validated @RequestBody DsFieldQueryBo bo) { + return R.ok(sysDatasourceService.queryFieldData(bo)); + } +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/domain/SysDatasource.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/domain/SysDatasource.java new file mode 100644 index 000000000..36ab56e15 --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/domain/SysDatasource.java @@ -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; + + +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/domain/bo/DsFieldQueryBo.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/domain/bo/DsFieldQueryBo.java new file mode 100644 index 000000000..271a07f89 --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/domain/bo/DsFieldQueryBo.java @@ -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; +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/domain/bo/DsQueryBo.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/domain/bo/DsQueryBo.java new file mode 100644 index 000000000..3131aa1d6 --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/domain/bo/DsQueryBo.java @@ -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; +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/domain/bo/FieldQueryBo.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/domain/bo/FieldQueryBo.java new file mode 100644 index 000000000..7d49fd7a4 --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/domain/bo/FieldQueryBo.java @@ -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 columns; + + private List fieldConditions; + + private List fieldOrders; + + private List offsets; + + private Integer limit; + + private List 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; + } +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/domain/bo/SysDatasourceBo.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/domain/bo/SysDatasourceBo.java new file mode 100644 index 000000000..9eb8776a6 --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/domain/bo/SysDatasourceBo.java @@ -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; + + +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/domain/vo/SysDatasourceVo.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/domain/vo/SysDatasourceVo.java new file mode 100644 index 000000000..7582be907 --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/domain/vo/SysDatasourceVo.java @@ -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; + + +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/DatabaseException.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/DatabaseException.java new file mode 100644 index 000000000..f548f884a --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/DatabaseException.java @@ -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); + } +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/JdbcQuery.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/JdbcQuery.java new file mode 100644 index 000000000..f16468a5e --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/JdbcQuery.java @@ -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> queryForPage(IPage> 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> maps = jdbc.queryForList(sql + " LIMIT " + page.offset() + StringPool.COMMA + page.getSize()); + page.setRecords(maps); + } + return page; + } + + /** + * 分页查询 + */ + default IPage queryForPage(IPage page, String sql, Class 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 rowMapper = BeanPropertyRowMapper.newInstance(elementType); + List 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 queryDatabases() throws DatabaseException { + try { + return getJdbc().queryForList("show databases", String.class); + } catch (Exception e) { + throw new DatabaseException(e); + } + } + + /** + * 查询表数据, 当前数据库 + */ + default List> queryTables(String... tables) throws DatabaseException { + return querySchemaTables(null, tables); + } + + /** + * 查询表数据 + */ + default List> querySchemaTables(String schema, String... tables) throws DatabaseException { + try { + return getJdbc().queryForList(sqlQueryTable(schema, tables)); + } catch (Exception e) { + throw new DatabaseException(e); + } + } + + /** + * 分页查询表数据 + */ + default IPage> queryTablePage(IPage> 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> queryTableFields(String schema, String table) throws DatabaseException { + throw ExceptionUtils.methodNotImplemented(); + } + + /** + * 获取数据类型 + */ + default List queryDateTypes() throws DatabaseException { + throw ExceptionUtils.methodNotImplemented(); + } + + /** + * 获取引擎类型 + */ + default List queryEngines() throws DatabaseException { + throw ExceptionUtils.methodNotImplemented(); + } + + /** + * 获取数据库内置方法 + */ + default List queryFunctions() throws DatabaseException { + throw ExceptionUtils.methodNotImplemented(); + } + + /** + * 获取数据库集群数据 + */ + default List queryClusters() throws DatabaseException { + throw ExceptionUtils.methodNotImplemented(); + } + + /** + * 将map装换为javabean对象 + */ + default T mapToBean(Map map, Class clz) { + try { + return mapToBean(map, clz.newInstance()); + } catch (InstantiationException | IllegalAccessException ignored) { + return null; + } + } + + default T mapToBean(Map 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 Map beanToMap(T bean) { + Map 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; + } +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/JdbcQueryBuilder.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/JdbcQueryBuilder.java new file mode 100644 index 000000000..b2779db2b --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/JdbcQueryBuilder.java @@ -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; + } + } +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/JdbcService.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/JdbcService.java new file mode 100644 index 000000000..2d239b74a --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/JdbcService.java @@ -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 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 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 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 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 query(String dsName, String dsType, Function func) { + try { + DynamicDataSourceContextHolder.push(dsName); + return func.apply(getJdbcQuery(dsName, dsType)); + } finally { + DynamicDataSourceContextHolder.clear(); + } + } + + /** + * 执行,没有返回值 + */ + public void execute(String dsName, String dsType, Consumer 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; + } +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/core/DataType.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/core/DataType.java new file mode 100644 index 000000000..3d3e0d8f8 --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/core/DataType.java @@ -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; +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/core/DbType.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/core/DbType.java new file mode 100644 index 000000000..2cf9b81b9 --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/core/DbType.java @@ -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; + } +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/core/InternalFunction.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/core/InternalFunction.java new file mode 100644 index 000000000..bbcbd3757 --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/core/InternalFunction.java @@ -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; +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/core/StorageEngine.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/core/StorageEngine.java new file mode 100644 index 000000000..41b24c3db --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/core/StorageEngine.java @@ -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> params; +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/query/ClickHouseJdbcTemplate.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/query/ClickHouseJdbcTemplate.java new file mode 100644 index 000000000..2ee160703 --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/query/ClickHouseJdbcTemplate.java @@ -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> 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 queryDateTypes() throws DatabaseException { + return Arrays.stream(ClickHouseDataType.values()).map(type -> new DataType(type.name(), type.isSigned())).collect(Collectors.toList()); + } + + @Override + public List queryEngines() throws DatabaseException { + String sql = "select name, comment from system.storage_engines"; + return getJdbc().query(sql, BeanPropertyRowMapper.newInstance(StorageEngine.class)); + } + + @Override + public List 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 queryClusters() throws DatabaseException { + String sql = "select distinct cluster from system.clusters"; + return getJdbc().queryForList(sql, String.class); + } +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/query/MysqlJdbcTemplate.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/query/MysqlJdbcTemplate.java new file mode 100644 index 000000000..8370570d6 --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/jdbc/query/MysqlJdbcTemplate.java @@ -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> 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 queryDateTypes() throws DatabaseException { + return Arrays.stream(MysqlType.values()).map(type -> new DataType(type.getName(), type.getCreateParams().contains("UNSIGNED"))).collect(Collectors.toList()); + } + + @Override + public List 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()); + } +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/mapper/SysDatasourceMapper.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/mapper/SysDatasourceMapper.java new file mode 100644 index 000000000..e6df0f790 --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/mapper/SysDatasourceMapper.java @@ -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 { + +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/service/ISysDatasourceService.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/service/ISysDatasourceService.java new file mode 100644 index 000000000..300b9b658 --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/service/ISysDatasourceService.java @@ -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 queryPageList(SysDatasourceBo bo, PageQuery pageQuery); + + /** + * 查询动态数据源列表 + */ + List queryList(SysDatasourceBo bo); + + /** + * 新增动态数据源 + */ + Boolean insertByBo(SysDatasourceBo bo); + + /** + * 修改动态数据源 + */ + Boolean updateByBo(SysDatasourceBo bo); + + /** + * 校验并批量删除动态数据源信息 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 获取JdbcService + */ + JdbcService getJdbcService(); + + /** + * 测试连接 + */ + Boolean testConnByBo(SysDatasourceBo bo); + + /** + * 查询字段数据 + */ + Object queryFieldData(DsFieldQueryBo bo); +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/service/impl/SysDatasourceServiceImpl.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/service/impl/SysDatasourceServiceImpl.java new file mode 100644 index 000000000..d13c7bb76 --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/service/impl/SysDatasourceServiceImpl.java @@ -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 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.lambdaQuery().eq(SysDatasource::getDsName, name)); + } + + /** + * 查询动态数据源列表 + */ + @Override + public TableDataInfo queryPageList(SysDatasourceBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询动态数据源列表 + */ + @Override + public List queryList(SysDatasourceBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + private LambdaQueryWrapper buildQueryWrapper(SysDatasourceBo bo) { + Map params = bo.getParams(); + LambdaQueryWrapper 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 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(); + } +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/utils/Assert.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/utils/Assert.java new file mode 100644 index 000000000..3cdafa907 --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/utils/Assert.java @@ -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 + *

为 false 则抛出异常

+ * + * @param expression boolean 值 + * @param message 消息 + */ + public static void isTrue(boolean expression, String message, Object... params) { + if (!expression) { + throw ExceptionUtils.create(message, params); + } + } + + /** + * 断言这个 boolean 为 false + *

为 true 则抛出异常

+ * + * @param expression boolean 值 + * @param message 消息 + */ + public static void isFalse(boolean expression, String message, Object... params) { + isTrue(!expression, message, params); + } + + /** + * 断言这个 object 为 null + *

不为 null 则抛异常

+ * + * @param object 对象 + * @param message 消息 + */ + public static void isNull(Object object, String message, Object... params) { + isTrue(object == null, message, params); + } + + /** + * 断言这个 object 不为 null + *

为 null 则抛异常

+ * + * @param object 对象 + * @param message 消息 + */ + public static void notNull(Object object, String message, Object... params) { + isTrue(object != null, message, params); + } + + /** + * 断言这个 map 为 empty + *

为 empty 则抛异常

+ * + * @param map 集合 + * @param message 消息 + */ + public static void isEmpty(Map map, String message, Object... params) { + isTrue(CollUtil.isEmpty(map), message, params); + } + + /** + * 断言这个 collection 为 empty + *

为 empty 则抛异常

+ * + * @param collection 集合 + * @param message 消息 + */ + public static void isEmpty(Collection collection, String message, Object... params) { + isTrue(CollUtil.isEmpty(collection), message, params); + } + + /** + * 断言这个 value 不为 empty + *

为 empty 则抛异常

+ * + * @param value 字符串 + * @param message 消息 + */ + public static void notEmpty(String value, String message, Object... params) { + isTrue(StrUtil.isNotBlank(value), message, params); + } + + /** + * 断言这个 collection 不为 empty + *

为 empty 则抛异常

+ * + * @param collection 集合 + * @param message 消息 + */ + public static void notEmpty(Collection collection, String message, Object... params) { + isTrue(CollUtil.isNotEmpty(collection), message, params); + } + + /** + * 断言这个 map 不为 empty + *

为 empty 则抛异常

+ * + * @param map 集合 + * @param message 消息 + */ + public static void notEmpty(Map map, String message, Object... params) { + isTrue(CollUtil.isNotEmpty(map), message, params); + } + + /** + * 断言这个 数组 不为 empty + *

为 empty 则抛异常

+ * + * @param array 数组 + * @param message 消息 + */ + public static void notEmpty(Object[] array, String message, Object... params) { + isTrue(CollUtil.isNotEmpty(List.of(array)), message, params); + } +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/utils/ExceptionUtils.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/utils/ExceptionUtils.java new file mode 100644 index 000000000..5101c541e --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/utils/ExceptionUtils.java @@ -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"); + } +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/utils/SqlUtils.java b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/utils/SqlUtils.java new file mode 100644 index 000000000..12ef1e4f3 --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/java/org/dromara/datasource/utils/SqlUtils.java @@ -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 columnList = bo.getColumns(); + String[] columns = StrUtil.wrapAllWithPair(columnList.contains("*") ? "" : "`", columnList.toArray(new String[0])); + sqlBuilder.select(columns).from(table); + // 检索条件 + List 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 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> queryByOffsets(@NonNull JdbcTemplate jdbc, @NonNull String sql, List 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(); + } +} diff --git a/ruoyi-modules/ruoyi-datasource/src/main/resources/mapper/datasource/SysDatasourceMapper.xml b/ruoyi-modules/ruoyi-datasource/src/main/resources/mapper/datasource/SysDatasourceMapper.xml new file mode 100644 index 000000000..b65f20b27 --- /dev/null +++ b/ruoyi-modules/ruoyi-datasource/src/main/resources/mapper/datasource/SysDatasourceMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/script/sql/ry_vue_5.X.sql b/script/sql/ry_vue_5.X.sql index 618b6c7e2..125b45949 100644 --- a/script/sql/ry_vue_5.X.sql +++ b/script/sql/ry_vue_5.X.sql @@ -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 ='动态数据源';