初始化

This commit is contained in:
2026-03-11 19:32:37 +08:00
commit 5708f80091
904 changed files with 68154 additions and 0 deletions

View File

@@ -0,0 +1,349 @@
# rdms-spring-boot-starter-mybatis
## 模块定位
`rdms-spring-boot-starter-mybatis` 不是一个“只把 MyBatis-Plus 引进来”的轻量 starter而是项目里的数据库访问基础设施模块。它把下面几类能力统一收口到一个地方
1. 数据源与事务管理
2. MyBatis-Plus 与 MyBatis-Plus-Join 的统一装配
3. 多数据库兼容策略
4. DO 基类、审计字段自动填充、逻辑删除
5. Mapper / Wrapper 的统一编码风格
6. 常用 TypeHandler
7. Easy-Trans 的 VO 翻译接入
从仓库里的实际用法看,这个模块倾向于让业务模块在 DAL 层遵循一套固定约定,而不是每个模块自己重复配置 MyBatis、分页、排序、主键策略和字段填充。
## 设计思路
## 1. 用一个 starter 统一数据库层约定
这个模块直接依赖了:
1. `druid-spring-boot-3-starter`
2. `mybatis-plus-spring-boot3-starter`
3. `dynamic-datasource-spring-boot3-starter`
4. `mybatis-plus-join-boot-starter`
5. `easy-trans-spring-boot-starter`
这说明模块不是“最小封装”路线,而是把项目默认采用的数据库栈整体打包,业务模块只需要依赖这一个 starter就能得到一致的运行时行为。
## 2. 优先解决“约定不一致”问题,而不是只提供工具类
模块里最核心的不是某一个工具类,而是整套约定:
1. `@MapperScan` 统一扫描 `${rdms.info.base-package}`
2. `BaseDO` 统一审计字段和逻辑删除字段
3. `BaseMapperX` 统一分页、批量操作、便捷查询
4. `LambdaQueryWrapperX` / `QueryWrapperX` 统一动态拼条件写法
5. `IdTypeEnvironmentPostProcessor` 统一不同数据库下的主键策略
这套设计可以让代码生成器、业务模块和框架模块都围绕同一套 DAL 模型工作。
## 3. 用“自动推断”减少多数据库切换成本
模块针对不同数据库运行场景做了几层兼容:
1. `pom.xml` 直接预留了 MySQL、Oracle、PostgreSQL、SQL Server、达梦、人大金仓、openGauss 等驱动
2. `IdTypeEnvironmentPostProcessor` 会根据主数据源 URL 自动判断 `DbType`
3. 如果 `mybatis-plus.global-config.db-config.id-type=NONE`,则自动改写为更适合当前数据库的 `AUTO``INPUT`
4. 同一个后处理器顺手补齐 Quartz 的 `driverDelegateClass`
5. `QueryWrapperX.limitN``MyBatisUtils.findInSet` 也在做跨数据库差异适配
也就是说,这个模块尽量不让业务代码关心“当前连接的是哪一种数据库”,而是把差异尽量前移到框架层。
## 4. 把“业务里高频重复的 Mapper 写法”沉到基类里
`BaseMapperX<T>` 是这个模块最重要的抽象之一。它在 `BaseMapper` 之上继续补了几类默认能力:
1. `selectPage(...)`:直接返回项目统一的 `PageResult`
2. `selectJoinPage(...)`:把 Join 查询也纳入同一分页模型
3. `selectOne` / `selectList` / `selectCount` 的便捷重载
4. `insertBatch` / `updateBatch` 等批量操作
5. 针对 SQL Server 的批量插入特殊处理
6. `PAGE_SIZE_NONE` 时的“不分页查询”约定
这背后的设计取向很明确:让 Mapper 接口本身承载一部分“轻业务语义”的默认实现,减少 XML 和 Service 层重复代码。
## 5. 动态查询要“少写 if”而不是把 if 搬到 Service 层
`LambdaQueryWrapperX``QueryWrapperX``MPJLambdaWrapperX` 都提供了 `xxxIfPresent` 系列方法,例如:
1. `likeIfPresent`
2. `eqIfPresent`
3. `inIfPresent`
4. `betweenIfPresent`
这样 Service 或 Mapper 默认方法可以直接链式拼接条件,空值自动跳过。这样可以把“查询条件存在才拼 SQL”这件事收口到 Wrapper而不是散落在业务代码里做大量 `if (param != null)`
## 6. 基础字段治理优先于业务字段治理
`BaseDO``DefaultDBFieldHandler` 体现了这个模块在数据治理上的几个固定要求:
1. 所有 DO 默认带 `createTime``updateTime``creator``updater``deleted`
2. 插入和更新时自动补时间
3. 已登录用户存在时自动补创建人和更新人
4. 逻辑删除统一使用 `deleted`,其未删除/已删除值由 yaml 中的 `logic-not-delete-value: 0``logic-delete-value: 1` 配置
5. `BaseDO.clean()` 用于清空前端可能误传回来的审计字段
这说明“审计字段一致性”被视为框架职责,而不是每个表、每个 Service 自己维护。
## 7. 常见字段存储形式做成透明 TypeHandler
模块内置了几类 `TypeHandler`
1. `EncryptTypeHandler`:字符串字段透明加解密
2. `StringListTypeHandler`
3. `LongListTypeHandler`
4. `LongSetTypeHandler`
5. `IntegerListTypeHandler`
6. `JacksonTypeHandler``ObjectMapper` 统一注入
这里的取向是数据库里允许保留“逗号分隔字符串”“JSON”“密文”等存储形式但业务对象层尽量继续使用自然的数据结构。
例如:
```java
@TableName(value = "infra_mail_account", autoResultMap = true)
public class MailAccountDO extends BaseDO {
@TableField(typeHandler = EncryptTypeHandler.class)
private String password;
@TableField(typeHandler = StringListTypeHandler.class)
private List<String> toMails;
}
```
如果数据库里的 `password` 存的是密文、`to_mails` 存的是 `a@xx.com,b@xx.com` 这样的逗号分隔字符串,
那么业务代码里仍然可以分别按普通 `String``List<String>` 来使用它们,字段转换交给 `TypeHandler` 处理。
## 8. 把“查询之后的展示翻译”也并入数据库 starter
`RdmsTranslateAutoConfiguration``TranslateUtils` 说明这个模块并不只关心“查出来”,还关心“查出来后如何转成面向前端的 VO”。
这是一种比较明显的项目式封装方式:把 DAL 和 VO 翻译放在同一个 starter方便后台管理类系统直接复用。
如果开发中需要这种能力,可以按下面的方式使用:
1. 在 VO 里同时定义“原始值字段”和“展示字段”,例如 `userId``userName`
2. 在原始值字段上增加 `@Trans`,声明要根据哪个对象、取哪个字段、回填到哪个展示字段
3. 查询完成后,先把 DO 转成 VO
4. 在返回前触发翻译。适合注解方式的接口可使用 `@TransMethodResult`,不适合注解方式的场景可手动调用 `TranslateUtils.translate(...)`
示例:
```java
public class OperateLogRespVO implements VO {
@Trans(type = TransType.SIMPLE, target = AdminUserDO.class, fields = "nickname", ref = "userName")
private Long userId;
private String userName;
}
```
```java
@TransMethodResult
public CommonResult<PageResult<OperateLogRespVO>> pageOperateLog(...) {
PageResult<OperateLogDO> pageResult = operateLogService.getOperateLogPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, OperateLogRespVO.class));
}
```
上面的含义是:返回结果里先保留 `userId`,然后在翻译阶段根据 `userId` 查到对应用户的 `nickname`,再回填到 `userName`
如果不是常规接口返回场景,例如导出 Excel可以在转换为 VO 后手动调用:
```java
TranslateUtils.translate(BeanUtils.toBean(list, OperateLogRespVO.class));
```
## 自动装配链路
## 1. Spring Boot 自动配置入口
`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`
1. `com.njcn.rdms.framework.datasource.config.RdmsDataSourceAutoConfiguration`
2. `com.njcn.rdms.framework.mybatis.config.RdmsMybatisAutoConfiguration`
3. `com.njcn.rdms.framework.translate.config.RdmsTranslateAutoConfiguration`
## 2. EnvironmentPostProcessor 入口
`META-INF/spring.factories`
1. `com.njcn.rdms.framework.mybatis.config.IdTypeEnvironmentPostProcessor`
这意味着模块启动时会先做环境预处理,再进入正常的自动配置流程。
## 核心组件
| 组件 | 作用 |
| --- | --- |
| `RdmsDataSourceAutoConfiguration` | 开启事务管理,并在启用 Druid 监控页时注册过滤器去掉广告脚本 |
| `RdmsMybatisAutoConfiguration` | 提前于 MyBatis-Plus 完成装配,统一 `@MapperScan`、分页插件、字段填充、主键生成器、`JacksonTypeHandler` |
| `IdTypeEnvironmentPostProcessor` | 根据主数据源 URL 推断数据库类型,自动设置 `id-type` 和 Quartz Delegate |
| `BaseDO` | 统一 DO 基类,内置审计字段、逻辑删除和 Easy-Trans 兼容处理 |
| `DefaultDBFieldHandler` | 自动填充创建人、更新人、创建时间、更新时间 |
| `BaseMapperX` | 统一分页、Join 分页、批量操作、便捷查询 |
| `LambdaQueryWrapperX` / `QueryWrapperX` / `MPJLambdaWrapperX` | 提供 `IfPresent` 风格的条件拼装能力 |
| `JdbcUtils` / `MyBatisUtils` | 统一数据库类型探测、分页构造、排序拼装、跨库 SQL 片段 |
| `EncryptTypeHandler` 等 | 处理密文、列表、集合等特殊字段映射 |
| `TranslateUtils` | 在不能用注解自动翻译时,手动触发 Easy-Trans 翻译 |
## 模块特征
从实现细节和业务模块的用法看,这个 starter 有几个比较明确的特征:
1. 它承担“项目默认 DAL 规范”的角色,而不是一个可随意裁剪的通用组件。
2. 它偏向“约定优于配置”例如主键策略、Mapper 扫描、分页结果、审计字段都由框架先定好。
3. 它存在一定程度的强绑定例如绑定动态数据源、Druid、Easy-Trans、Security 上下文,以换取更少的样板代码。
4. 它在多数据库兼容上投入较多,适合数据库切换、国产库适配或私有化部署场景。
5. 它优先抽象后台管理系统里高频出现的分页、筛选、列表页、字典翻译、逻辑删除、审计字段等能力。
## 如何使用
## 1. 引入依赖
通常业务模块直接依赖:
```xml
<dependency>
<groupId>com.njcn</groupId>
<artifactId>rdms-spring-boot-starter-mybatis</artifactId>
</dependency>
```
如果项目需要自动填充 `creator``updater`,运行时还应提供安全模块里的登录上下文能力。
## 2. 基础配置
最小可工作的配置重点有四个:
1. `rdms.info.base-package`
2. `spring.datasource.dynamic.primary`
3. `spring.datasource.dynamic.datasource.<primary>.url`
4. `mybatis-plus.global-config.db-config.id-type`
示例:
```yaml
rdms:
info:
base-package: com.njcn.rdms.module.system
spring:
datasource:
dynamic:
primary: master
datasource:
master:
url: jdbc:mysql://127.0.0.1:13306/rdms?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
mybatis-plus:
global-config:
db-config:
id-type: NONE
logic-delete-value: 1
logic-not-delete-value: 0
type-aliases-package: ${rdms.info.base-package}.dal.dataobject
encryptor:
password: your-16-bytes-key
mybatis-plus-join:
banner: false
easy-trans:
is-enable-global: false
```
说明:
1. `id-type: NONE` 是这里推荐的“自动判断模式”
2. 如果没有配置主数据源 URL`IdTypeEnvironmentPostProcessor` 就无法自动判断数据库类型
3. `encryptor.password` 只在使用 `EncryptTypeHandler` 时需要
## 3. DO 写法
推荐让 DO 继承 `BaseDO`,并在需要时启用 `autoResultMap = true`
```java
@TableName(value = "infra_data_source_config", autoResultMap = true)
@KeySequence("infra_data_source_config_seq")
public class DataSourceConfigDO extends BaseDO {
private Long id;
private String name;
private String url;
private String username;
@TableField(typeHandler = EncryptTypeHandler.class)
private String password;
}
```
如果字段在库里按逗号分隔字符串保存,也可以直接挂列表型 `TypeHandler`
```java
@TableField(typeHandler = StringListTypeHandler.class)
private List<String> toMails;
```
## 4. Mapper 写法
推荐所有 Mapper 继承 `BaseMapperX<T>`,把分页和条件拼装直接写在默认方法里:
```java
@Mapper
public interface AdminUserMapper extends BaseMapperX<AdminUserDO> {
default PageResult<AdminUserDO> selectPage(UserPageReqVO reqVO,
Collection<Long> deptIds,
Collection<Long> userIds) {
return selectPage(reqVO, new LambdaQueryWrapperX<AdminUserDO>()
.likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername())
.likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile())
.eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus())
.betweenIfPresent(AdminUserDO::getCreateTime, reqVO.getCreateTime())
.inIfPresent(AdminUserDO::getDeptId, deptIds)
.inIfPresent(AdminUserDO::getId, userIds)
.orderByDesc(AdminUserDO::getId));
}
}
```
如果需要跨库兼容的 `limit n`,当前实现建议使用 `QueryWrapperX`
```java
return selectList(new QueryWrapperX<NotifyMessageDO>()
.eq("user_id", userId)
.eq("user_type", userType)
.eq("read_status", false)
.orderByDesc("id")
.limitN(size));
```
## 5. 手动触发 VO 翻译
当场景不适合用注解自动翻译时,可以手动调用:
```java
List<OperateLogRespVO> result = TranslateUtils.translate(list);
```
## 典型开发流程
在这个 starter 的设计下,比较顺畅的开发流程基本是:
1. 配置好多数据源和 `rdms.info.base-package`
2. DO 继承 `BaseDO`
3. Mapper 继承 `BaseMapperX`
4. 查询条件优先使用 `WrapperX` 系列拼装
5. 分页统一返回 `PageResult`
6. 特殊字段优先通过 `TypeHandler` 做透明映射
7. VO 展示转换需要字典或名称翻译时再接 Easy-Trans
## 注意事项
1. `IdTypeEnvironmentPostProcessor` 依赖 `spring.datasource.dynamic.primary` 和对应数据源的 `url`,否则无法自动识别数据库类型。
2.`id-type` 被识别为 `INPUT` 时,自动配置还会注册对应数据库的 `IKeyGenerator`。这类场景下实体通常还需要正确声明 `@KeySequence`
3. `DefaultDBFieldHandler` 会调用安全上下文工具获取登录用户。如果运行时没有对应安全能力,创建人和更新人自动填充就不能按预期工作。
4. `EncryptTypeHandler` 依赖 `mybatis-plus.encryptor.password`。密钥一旦更换,历史密文可能无法解密,生产环境不应硬编码。
5. `easy-trans.is-enable-global` 在示例配置里默认关闭,说明默认没有把全局响应翻译作为高优先级能力。
6. `QueryWrapperX.limitN` 是跨数据库兼容封装,但它仍然是基于数据库方言分支处理,不适合无限扩展到所有数据库。
7. 这个模块的职责边界偏“大而全”适合本项目统一使用如果要抽成通用开源组件需要先拆掉对动态数据源、安全上下文、Easy-Trans 的强耦合。
## 总结
这个模块的核心价值,不是简单提供几个 MyBatis 工具类,而是把数据库层的共性问题一次性沉到框架层:
1. 启动时怎么自动装配
2. 多数据库怎么少改代码
3. 分页、排序、条件查询怎么统一写
4. 审计字段和逻辑删除怎么保持一致
5. 特殊字段怎么透明映射
6. 查询结果怎么更顺滑地进入 VO 展示层
如果把它当成一个“带强约束的项目级 DAL 基座”去理解,这个模块的设计就会非常清晰。

View File

@@ -0,0 +1,106 @@
<?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>com.njcn</groupId>
<artifactId>rdms-framework</artifactId>
<version>${revision}</version>
</parent>
<artifactId>rdms-spring-boot-starter-mybatis</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>数据库连接池、多数据源、事务、MyBatis 拓展</description>
<dependencies>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>rdms-common</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>com.njcn</groupId>
<artifactId>rdms-spring-boot-starter-security</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有 DefaultDBFieldHandler 使用到 -->
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.dameng</groupId>
<artifactId>DmJdbcDriver18</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.com.kingbase</groupId>
<artifactId>kingbase8</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.opengauss</groupId>
<artifactId>opengauss-jdbc</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId> <!-- 多数据源 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.github.yulichang</groupId>
<artifactId>mybatis-plus-join-boot-starter</artifactId> <!-- MyBatis 联表查询 -->
</dependency>
<dependency>
<groupId>com.fhs-opensource</groupId> <!-- VO 数据翻译 -->
<artifactId>easy-trans-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.fhs-opensource</groupId>
<artifactId>easy-trans-mybatis-plus-extend</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,40 @@
package com.njcn.rdms.framework.datasource.config;
import com.alibaba.druid.spring.boot3.autoconfigure.properties.DruidStatProperties;
import com.njcn.rdms.framework.datasource.core.filter.DruidAdRemoveFilter;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* 数据库配置类
*
* @author hongawen
*/
@AutoConfiguration
@EnableTransactionManagement(proxyTargetClass = true) // 启动事务管理
@EnableConfigurationProperties(DruidStatProperties.class)
public class RdmsDataSourceAutoConfiguration {
/**
* 创建 DruidAdRemoveFilter 过滤器,过滤 common.js 的广告
*/
@Bean
@ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled", havingValue = "true")
public FilterRegistrationBean<DruidAdRemoveFilter> druidAdRemoveFilterFilter(DruidStatProperties properties) {
// 获取 druid web 监控页面的参数
DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
// 提取 common.js 的配置路径
String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
// 创建 DruidAdRemoveFilter Bean
FilterRegistrationBean<DruidAdRemoveFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new DruidAdRemoveFilter());
registrationBean.addUrlPatterns(commonJsPattern);
return registrationBean;
}
}

View File

@@ -0,0 +1,22 @@
package com.njcn.rdms.framework.datasource.core.enums;
/**
* 对应于多数据源中不同数据源配置
*
* 通过在方法上,使用 {@link com.baomidou.dynamic.datasource.annotation.DS} 注解,设置使用的数据源。
* 注意,默认是 {@link #MASTER} 数据源
*
* 对应官方文档为 http://dynamic-datasource.com/guide/customize/Annotation.html
*/
public interface DataSourceEnum {
/**
* 主库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Master} 注解
*/
String MASTER = "master";
/**
* 从库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Slave} 注解
*/
String SLAVE = "slave";
}

View File

@@ -0,0 +1,38 @@
package com.njcn.rdms.framework.datasource.core.filter;
import com.alibaba.druid.util.Utils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* Druid 底部广告过滤器
*
* @author hongawen
*/
public class DruidAdRemoveFilter extends OncePerRequestFilter {
/**
* common.js 的路径
*/
private static final String COMMON_JS_ILE_PATH = "support/http/resources/js/common.js";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
chain.doFilter(request, response);
// 重置缓冲区,响应头不会被重置
response.resetBuffer();
// 获取 common.js
String text = Utils.readFromResource(COMMON_JS_ILE_PATH);
// 正则替换 banner, 除去底部的广告信息
text = text.replaceAll("<a.*?banner\"></a><br/>", "");
text = text.replaceAll("powered.*?shrek.wang</a>", "");
response.getWriter().write(text);
}
}

View File

@@ -0,0 +1,5 @@
/**
* 数据库连接池,采用 Druid
* 多数据源,采用爆米花
*/
package com.njcn.rdms.framework.datasource;

View File

@@ -0,0 +1,119 @@
package com.njcn.rdms.framework.mybatis.config;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import com.njcn.rdms.framework.common.util.collection.SetUtils;
import com.njcn.rdms.framework.mybatis.core.util.JdbcUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* 当 IdType 为 {@link IdType#NONE} 时,根据 PRIMARY 数据源所使用的数据库,自动设置
*
* @author hongawen
*/
@Slf4j
public class IdTypeEnvironmentPostProcessor implements EnvironmentPostProcessor {
private static final String ID_TYPE_KEY = "mybatis-plus.global-config.db-config.id-type";
private static final String DATASOURCE_DYNAMIC_KEY = "spring.datasource.dynamic";
private static final String QUARTZ_JOB_STORE_DRIVER_KEY = "spring.quartz.properties.org.quartz.jobStore.driverDelegateClass";
private static final Set<DbType> INPUT_ID_TYPES = SetUtils.asSet(DbType.ORACLE, DbType.ORACLE_12C,
DbType.POSTGRE_SQL, DbType.KINGBASE_ES, DbType.DB2, DbType.H2);
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
// 如果获取不到 DbType则不进行处理
DbType dbType = getDbType(environment);
if (dbType == null) {
return;
}
// 设置 Quartz JobStore 对应的 Driver
// TODO 暂时没有找到特别合适的地方,先放在这里
setJobStoreDriverIfPresent(environment, dbType);
// 如果非 NONE则不进行处理
IdType idType = getIdType(environment);
if (idType != IdType.NONE) {
return;
}
// 情况一,用户输入 ID适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库
if (INPUT_ID_TYPES.contains(dbType)) {
setIdType(environment, IdType.INPUT);
return;
}
// 情况二,自增 ID适合 MySQL、DM 达梦等直接自增的数据库
setIdType(environment, IdType.AUTO);
}
public IdType getIdType(ConfigurableEnvironment environment) {
String value = environment.getProperty(ID_TYPE_KEY);
try {
return StrUtil.isNotBlank(value) ? IdType.valueOf(value) : IdType.NONE;
} catch (IllegalArgumentException ex) {
log.error("[getIdType][无法解析 id-type 配置值({})]", value, ex);
return IdType.NONE;
}
}
public void setIdType(ConfigurableEnvironment environment, IdType idType) {
Map<String, Object> map = new HashMap<>();
map.put(ID_TYPE_KEY, idType);
environment.getPropertySources().addFirst(new MapPropertySource("mybatisPlusIdType", map));
log.info("[setIdType][修改 MyBatis Plus 的 idType 为({})]", idType);
}
public void setJobStoreDriverIfPresent(ConfigurableEnvironment environment, DbType dbType) {
String driverClass = environment.getProperty(QUARTZ_JOB_STORE_DRIVER_KEY);
if (StrUtil.isNotEmpty(driverClass)) {
return;
}
// 根据 dbType 类型,获取对应的 driverClass
switch (dbType) {
case POSTGRE_SQL:
driverClass = "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate";
break;
case ORACLE:
case ORACLE_12C:
driverClass = "org.quartz.impl.jdbcjobstore.oracle.OracleDelegate";
break;
case SQL_SERVER:
case SQL_SERVER2005:
driverClass = "org.quartz.impl.jdbcjobstore.MSSQLDelegate";
break;
case DM:
case KINGBASE_ES:
driverClass = "org.quartz.impl.jdbcjobstore.StdJDBCDelegate";
break;
}
// 设置 driverClass 变量
if (StrUtil.isNotEmpty(driverClass)) {
environment.getSystemProperties().put(QUARTZ_JOB_STORE_DRIVER_KEY, driverClass);
}
}
public static DbType getDbType(ConfigurableEnvironment environment) {
String primary = environment.getProperty(DATASOURCE_DYNAMIC_KEY + "." + "primary");
if (StrUtil.isEmpty(primary)) {
return null;
}
String url = environment.getProperty(DATASOURCE_DYNAMIC_KEY + ".datasource." + primary + ".url");
if (StrUtil.isEmpty(url)) {
return null;
}
return JdbcUtils.getDbType(url);
}
}

View File

@@ -0,0 +1,94 @@
package com.njcn.rdms.framework.mybatis.config;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.baomidou.mybatisplus.extension.incrementer.*;
import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal;
import com.baomidou.mybatisplus.extension.parser.cache.JdkSerialCaffeineJsqlParseCache;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.njcn.rdms.framework.common.util.json.JsonUtils;
import com.njcn.rdms.framework.mybatis.core.handler.DefaultDBFieldHandler;
import org.apache.ibatis.annotations.Mapper;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.ConfigurableEnvironment;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* MyBaits 配置类
*
* @author hongawen
*/
@AutoConfiguration(before = MybatisPlusAutoConfiguration.class) // 目的:先于 MyBatis Plus 自动配置,避免 @MapperScan 可能扫描不到 Mapper 打印 warn 日志
@MapperScan(value = "${rdms.info.base-package}", annotationClass = Mapper.class,
lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试
public class RdmsMybatisAutoConfiguration {
static {
// 动态 SQL 智能优化支持本地缓存加速解析,更完善的租户复杂 XML 动态 SQL 支持,静态注入缓存
JsqlParserGlobal.setJsqlParseCache(new JdkSerialCaffeineJsqlParseCache(
(cache) -> cache.maximumSize(1024)
.expireAfterWrite(5, TimeUnit.SECONDS))
);
}
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件
// ↓↓↓ 按需开启,可能会影响到 updateBatch 的地方:例如说文件配置管理 ↓↓↓
// mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 拦截没有指定条件的 update 和 delete 语句
return mybatisPlusInterceptor;
}
@Bean
public MetaObjectHandler defaultMetaObjectHandler() {
return new DefaultDBFieldHandler(); // 自动填充参数类
}
@Bean
@ConditionalOnProperty(prefix = "mybatis-plus.global-config.db-config", name = "id-type", havingValue = "INPUT")
public IKeyGenerator keyGenerator(ConfigurableEnvironment environment) {
DbType dbType = IdTypeEnvironmentPostProcessor.getDbType(environment);
if (dbType != null) {
switch (dbType) {
case POSTGRE_SQL:
return new PostgreKeyGenerator();
case ORACLE:
case ORACLE_12C:
return new OracleKeyGenerator();
case H2:
return new H2KeyGenerator();
case KINGBASE_ES:
return new KingbaseKeyGenerator();
case DM:
return new DmKeyGenerator();
}
}
// 找不到合适的 IKeyGenerator 实现类
throw new IllegalArgumentException(StrUtil.format("DbType{} 找不到合适的 IKeyGenerator 实现类", dbType));
}
@Bean // 特殊:返回结果使用 Object 而不用 JacksonTypeHandler 的原因,避免因为 JacksonTypeHandler 被 mybatis 全局使用!
public Object jacksonTypeHandler(List<ObjectMapper> objectMappers) {
// 特殊:设置 JacksonTypeHandler 的 ObjectMapper
ObjectMapper objectMapper = CollUtil.getFirst(objectMappers);
if (objectMapper == null) {
objectMapper = JsonUtils.getObjectMapper();
}
JacksonTypeHandler.setObjectMapper(objectMapper);
return new JacksonTypeHandler(Object.class);
}
}

View File

@@ -0,0 +1,66 @@
package com.njcn.rdms.framework.mybatis.core.dataobject;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fhs.core.trans.vo.TransPojo;
import lombok.Data;
import org.apache.ibatis.type.JdbcType;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 基础实体对象
*
* 为什么实现 {@link TransPojo} 接口?
* 因为使用 Easy-Trans TransType.SIMPLE 模式,集成 MyBatis Plus 查询
*
* @author hongawen
*/
@Data
@JsonIgnoreProperties(value = "transMap") // 由于 Easy-Trans 会添加 transMap 属性,避免 Jackson 在 Spring Cache 反序列化报错
public abstract class BaseDO implements Serializable, TransPojo {
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 最后更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 创建者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
@TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR)
private String creator;
/**
* 更新者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
@TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.VARCHAR)
private String updater;
/**
* 是否删除
*/
@TableLogic
private Boolean deleted;
/**
* 把 creator、createTime、updateTime、updater 都清空,避免前端直接传递 creator 之类的字段,直接就被更新了
*/
public void clean(){
this.creator = null;
this.createTime = null;
this.updater = null;
this.updateTime = null;
}
}

View File

@@ -0,0 +1,101 @@
package com.njcn.rdms.framework.mybatis.core.enums;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.DbType;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 针对 MyBatis Plus 的 {@link DbType} 增强,补充更多信息
*/
@Getter
@AllArgsConstructor
public enum DbTypeEnum {
/**
* H2
*
* 注意H2 不支持 find_in_set 函数
*/
H2(DbType.H2, "H2", ""),
/**
* MySQL
*/
MY_SQL(DbType.MYSQL, "MySQL", "FIND_IN_SET('#{value}', #{column}) <> 0"),
/**
* Oracle
*/
ORACLE(DbType.ORACLE, "Oracle", "FIND_IN_SET('#{value}', #{column}) <> 0"),
/**
* PostgreSQL
*
* 华为 openGauss 使用 ProductName 与 PostgreSQL 相同
*/
POSTGRE_SQL(DbType.POSTGRE_SQL,"PostgreSQL", "POSITION('#{value}' IN #{column}) <> 0"),
/**
* SQL Server
*/
SQL_SERVER(DbType.SQL_SERVER, "Microsoft SQL Server", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"),
/**
* SQL Server 2005
*/
SQL_SERVER2005(DbType.SQL_SERVER2005, "Microsoft SQL Server 2005", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"),
/**
* 达梦
*/
DM(DbType.DM, "DM DBMS", "FIND_IN_SET('#{value}', #{column}) <> 0"),
/**
* 人大金仓
*/
KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION('#{value}' IN #{column}) <> 0"),
/**
* OceanBase
*/
OCEAN_BASE(DbType.OCEAN_BASE, "OceanBase", "FIND_IN_SET('#{value}', #{column}) <> 0")
;
public static final Map<String, DbTypeEnum> MAP_BY_NAME = Arrays.stream(values())
.collect(Collectors.toMap(DbTypeEnum::getProductName, Function.identity()));
public static final Map<DbType, DbTypeEnum> MAP_BY_MP = Arrays.stream(values())
.collect(Collectors.toMap(DbTypeEnum::getMpDbType, Function.identity()));
/**
* MyBatis Plus 类型
*/
private final DbType mpDbType;
/**
* 数据库产品名
*/
private final String productName;
/**
* SQL FIND_IN_SET 模板
*/
private final String findInSetTemplate;
public static DbType find(String databaseProductName) {
if (StrUtil.isBlank(databaseProductName)) {
return null;
}
return MAP_BY_NAME.get(databaseProductName).getMpDbType();
}
public static String getFindInSetTemplate(DbType dbType) {
return Optional.of(MAP_BY_MP.get(dbType).getFindInSetTemplate())
.orElseThrow(() -> new IllegalArgumentException("FIND_IN_SET not supported"));
}
}

View File

@@ -0,0 +1,67 @@
package com.njcn.rdms.framework.mybatis.core.handler;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import java.time.LocalDateTime;
import java.util.Objects;
/**
* 通用参数填充实现类
*
* 如果没有显式的对通用参数进行赋值,这里会对通用参数进行填充、赋值
*
* @author hexiaowu
*/
public class DefaultDBFieldHandler implements MetaObjectHandler {
@Override
@SuppressWarnings("PatternVariableCanBeUsed")
public void insertFill(MetaObject metaObject) {
if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();
LocalDateTime current = LocalDateTime.now();
// 创建时间为空,则以当前时间为插入时间
if (Objects.isNull(baseDO.getCreateTime())) {
baseDO.setCreateTime(current);
}
// 更新时间为空,则以当前时间为更新时间
if (Objects.isNull(baseDO.getUpdateTime())) {
baseDO.setUpdateTime(current);
}
// deleted 为空时,统一按未删除处理,避免依赖数据库默认值
if (Objects.isNull(baseDO.getDeleted())) {
baseDO.setDeleted(Boolean.FALSE);
}
Long userId = SecurityFrameworkUtils.getLoginUserId();
// 当前登录用户不为空,创建人为空,则当前登录用户为创建人
if (Objects.nonNull(userId) && Objects.isNull(baseDO.getCreator())) {
baseDO.setCreator(userId.toString());
}
// 当前登录用户不为空,更新人为空,则当前登录用户为更新人
if (Objects.nonNull(userId) && Objects.isNull(baseDO.getUpdater())) {
baseDO.setUpdater(userId.toString());
}
}
}
@Override
public void updateFill(MetaObject metaObject) {
// 更新时间为空,则以当前时间为更新时间
Object modifyTime = getFieldValByName("updateTime", metaObject);
if (Objects.isNull(modifyTime)) {
setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
}
// 当前登录用户不为空,更新人为空,则当前登录用户为更新人
Object modifier = getFieldValByName("updater", metaObject);
Long userId = SecurityFrameworkUtils.getLoginUserId();
if (Objects.nonNull(userId) && Objects.isNull(modifier)) {
setFieldValByName("updater", userId.toString(), metaObject);
}
}
}

View File

@@ -0,0 +1,249 @@
package com.njcn.rdms.framework.mybatis.core.mapper;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.extension.toolkit.Db;
import com.github.yulichang.base.MPJBaseMapper;
import com.github.yulichang.interfaces.MPJBaseJoin;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import com.njcn.rdms.framework.common.pojo.PageParam;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.common.pojo.SortablePageParam;
import com.njcn.rdms.framework.common.pojo.SortingField;
import com.njcn.rdms.framework.mybatis.core.util.JdbcUtils;
import com.njcn.rdms.framework.mybatis.core.util.MyBatisUtils;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
import java.util.List;
/**
* 在 MyBatis Plus 的 BaseMapper 的基础上拓展,提供更多的能力
*
* 1. {@link BaseMapper} 为 MyBatis Plus 的基础接口,提供基础的 CRUD 能力
* 2. {@link MPJBaseMapper} 为 MyBatis Plus Join 的基础接口,提供连表 Join 能力
*/
public interface BaseMapperX<T> extends MPJBaseMapper<T> {
default PageResult<T> selectPage(SortablePageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) {
return selectPage(pageParam, pageParam.getSortingFields(), queryWrapper);
}
default PageResult<T> selectPage(PageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) {
return selectPage(pageParam, null, queryWrapper);
}
default PageResult<T> selectPage(PageParam pageParam, Collection<SortingField> sortingFields, @Param("ew") Wrapper<T> queryWrapper) {
// 特殊:不分页,直接查询全部
if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) {
MyBatisUtils.addOrder(queryWrapper, sortingFields);
List<T> list = selectList(queryWrapper);
return new PageResult<>(list, (long) list.size());
}
// MyBatis Plus 查询
IPage<T> mpPage = MyBatisUtils.buildPage(pageParam, sortingFields);
selectPage(mpPage, queryWrapper);
// 转换返回
return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
}
default <D> PageResult<D> selectJoinPage(PageParam pageParam, Class<D> clazz, MPJLambdaWrapper<T> lambdaWrapper) {
// 特殊:不分页,直接查询全部
if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) {
List<D> list = selectJoinList(clazz, lambdaWrapper);
return new PageResult<>(list, (long) list.size());
}
// MyBatis Plus Join 查询
IPage<D> mpPage = MyBatisUtils.buildPage(pageParam);
mpPage = selectJoinPage(mpPage, clazz, lambdaWrapper);
// 转换返回
return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
}
/**
* 执行分页查询并返回结果。
*
* @param pageParam 分页参数,包含页码、每页条数和排序字段信息。如果 pageSize 为 {@link PageParam#PAGE_SIZE_NONE},则不分页,直接查询所有数据。
* @param clazz 结果集的类类型
* @param lambdaWrapper MyBatis Plus Join 查询条件包装器
* @param <D> 结果集的泛型类型
* @return 返回分页查询的结果,包括总记录数和当前页的数据列表
*/
default <D> PageResult<D> selectJoinPage(SortablePageParam pageParam, Class<D> clazz, MPJLambdaWrapper<T> lambdaWrapper) {
// 特殊:不分页,直接查询全部
if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) {
List<D> list = selectJoinList(clazz, lambdaWrapper);
return new PageResult<>(list, (long) list.size());
}
// MyBatis Plus Join 查询
IPage<D> mpPage = MyBatisUtils.buildPage(pageParam, pageParam.getSortingFields());
mpPage = selectJoinPage(mpPage, clazz, lambdaWrapper);
// 转换返回
return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
}
default <DTO> PageResult<DTO> selectJoinPage(PageParam pageParam, Class<DTO> resultTypeClass, MPJBaseJoin<T> joinQueryWrapper) {
IPage<DTO> mpPage = MyBatisUtils.buildPage(pageParam);
selectJoinPage(mpPage, resultTypeClass, joinQueryWrapper);
// 转换返回
return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
}
default T selectOne(String field, Object value) {
return selectOne(new QueryWrapper<T>().eq(field, value));
}
default T selectOne(SFunction<T, ?> field, Object value) {
return selectOne(new LambdaQueryWrapper<T>().eq(field, value));
}
default T selectOne(String field1, Object value1, String field2, Object value2) {
return selectOne(new QueryWrapper<T>().eq(field1, value1).eq(field2, value2));
}
default T selectOne(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) {
return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2));
}
default T selectOne(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2,
SFunction<T, ?> field3, Object value3) {
return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2).eq(field3, value3));
}
/**
* 获取满足条件的第 1 条记录
*
* 目的:解决并发场景下,插入多条记录后,使用 selectOne 会报错的问题
*
* @param field 字段名
* @param value 字段值
* @return 实体
*/
default T selectFirstOne(SFunction<T, ?> field, Object value) {
// 如果明确使用 MySQL 等场景,可以考虑使用 LIMIT 1 进行优化
List<T> list = selectList(new LambdaQueryWrapper<T>().eq(field, value));
return CollUtil.getFirst(list);
}
default T selectFirstOne(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) {
List<T> list = selectList(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2));
return CollUtil.getFirst(list);
}
default T selectFirstOne(SFunction<T,?> field1, Object value1, SFunction<T,?> field2, Object value2,
SFunction<T,?> field3, Object value3) {
List<T> list = selectList(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2).eq(field3, value3));
return CollUtil.getFirst(list);
}
default Long selectCount() {
return selectCount(new QueryWrapper<>());
}
default Long selectCount(String field, Object value) {
return selectCount(new QueryWrapper<T>().eq(field, value));
}
default Long selectCount(SFunction<T, ?> field, Object value) {
return selectCount(new LambdaQueryWrapper<T>().eq(field, value));
}
default List<T> selectList() {
return selectList(new QueryWrapper<>());
}
default List<T> selectList(String field, Object value) {
return selectList(new QueryWrapper<T>().eq(field, value));
}
default List<T> selectList(SFunction<T, ?> field, Object value) {
return selectList(new LambdaQueryWrapper<T>().eq(field, value));
}
default List<T> selectList(String field, Collection<?> values) {
if (CollUtil.isEmpty(values)) {
return CollUtil.newArrayList();
}
return selectList(new QueryWrapper<T>().in(field, values));
}
default List<T> selectList(SFunction<T, ?> field, Collection<?> values) {
if (CollUtil.isEmpty(values)) {
return CollUtil.newArrayList();
}
return selectList(new LambdaQueryWrapper<T>().in(field, values));
}
default List<T> selectList(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) {
return selectList(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2));
}
/**
* 批量插入,适合大量数据插入
*
* @param entities 实体们
*/
default Boolean insertBatch(Collection<T> entities) {
// 特殊SQL Server 批量插入后,获取 id 会报错,因此通过循环处理
DbType dbType = JdbcUtils.getDbType();
if (JdbcUtils.isSQLServer(dbType)) {
entities.forEach(this::insert);
return CollUtil.isNotEmpty(entities);
}
return Db.saveBatch(entities);
}
/**
* 批量插入,适合大量数据插入
*
* @param entities 实体们
* @param size 插入数量 Db.saveBatch 默认为 1000
*/
default Boolean insertBatch(Collection<T> entities, int size) {
// 特殊SQL Server 批量插入后,获取 id 会报错,因此通过循环处理
DbType dbType = JdbcUtils.getDbType();
if (JdbcUtils.isSQLServer(dbType)) {
entities.forEach(this::insert);
return CollUtil.isNotEmpty(entities);
}
return Db.saveBatch(entities, size);
}
default int updateBatch(T update) {
return update(update, new QueryWrapper<>());
}
default Boolean updateBatch(Collection<T> entities) {
return Db.updateBatchById(entities);
}
default Boolean updateBatch(Collection<T> entities, int size) {
return Db.updateBatchById(entities, size);
}
default int delete(String field, String value) {
return delete(new QueryWrapper<T>().eq(field, value));
}
default int delete(SFunction<T, ?> field, Object value) {
return delete(new LambdaQueryWrapper<T>().eq(field, value));
}
default int deleteBatch(SFunction<T, ?> field, Collection<?> values) {
if (CollUtil.isEmpty(values)) {
return 0;
}
return delete(new LambdaQueryWrapper<T>().in(field, values));
}
}

View File

@@ -0,0 +1,135 @@
package com.njcn.rdms.framework.mybatis.core.query;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.njcn.rdms.framework.common.util.collection.ArrayUtils;
import org.springframework.util.StringUtils;
import java.util.Collection;
/**
* 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能:
* <p>
* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
*
* @param <T> 数据类型
*/
public class LambdaQueryWrapperX<T> extends LambdaQueryWrapper<T> {
public LambdaQueryWrapperX<T> likeIfPresent(SFunction<T, ?> column, String val) {
if (StringUtils.hasText(val)) {
return (LambdaQueryWrapperX<T>) super.like(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> inIfPresent(SFunction<T, ?> column, Collection<?> values) {
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
return (LambdaQueryWrapperX<T>) super.in(column, values);
}
return this;
}
public LambdaQueryWrapperX<T> inIfPresent(SFunction<T, ?> column, Object... values) {
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
return (LambdaQueryWrapperX<T>) super.in(column, values);
}
return this;
}
public LambdaQueryWrapperX<T> eqIfPresent(SFunction<T, ?> column, Object val) {
if (ObjectUtil.isNotEmpty(val)) {
return (LambdaQueryWrapperX<T>) super.eq(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> neIfPresent(SFunction<T, ?> column, Object val) {
if (ObjectUtil.isNotEmpty(val)) {
return (LambdaQueryWrapperX<T>) super.ne(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> gtIfPresent(SFunction<T, ?> column, Object val) {
if (val != null) {
return (LambdaQueryWrapperX<T>) super.gt(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> geIfPresent(SFunction<T, ?> column, Object val) {
if (val != null) {
return (LambdaQueryWrapperX<T>) super.ge(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> ltIfPresent(SFunction<T, ?> column, Object val) {
if (val != null) {
return (LambdaQueryWrapperX<T>) super.lt(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> leIfPresent(SFunction<T, ?> column, Object val) {
if (val != null) {
return (LambdaQueryWrapperX<T>) super.le(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object val1, Object val2) {
if (val1 != null && val2 != null) {
return (LambdaQueryWrapperX<T>) super.between(column, val1, val2);
}
if (val1 != null) {
return (LambdaQueryWrapperX<T>) ge(column, val1);
}
if (val2 != null) {
return (LambdaQueryWrapperX<T>) le(column, val2);
}
return this;
}
public LambdaQueryWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object[] values) {
Object val1 = ArrayUtils.get(values, 0);
Object val2 = ArrayUtils.get(values, 1);
return betweenIfPresent(column, val1, val2);
}
// ========== 重写父类方法,方便链式调用 ==========
@Override
public LambdaQueryWrapperX<T> eq(boolean condition, SFunction<T, ?> column, Object val) {
super.eq(condition, column, val);
return this;
}
@Override
public LambdaQueryWrapperX<T> eq(SFunction<T, ?> column, Object val) {
super.eq(column, val);
return this;
}
@Override
public LambdaQueryWrapperX<T> orderByDesc(SFunction<T, ?> column) {
super.orderByDesc(true, column);
return this;
}
@Override
public LambdaQueryWrapperX<T> last(String lastSql) {
super.last(lastSql);
return this;
}
@Override
public LambdaQueryWrapperX<T> in(SFunction<T, ?> column, Collection<?> coll) {
super.in(column, coll);
return this;
}
}

View File

@@ -0,0 +1,355 @@
package com.njcn.rdms.framework.mybatis.core.query;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import com.njcn.rdms.framework.common.util.collection.ArrayUtils;
import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.function.Consumer;
/**
* 拓展 MyBatis Plus Join QueryWrapper 类,主要增加如下功能:
* <p>
* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
* 2. SFunction<S, ?> column + <S> 泛型:支持任意类字段(主表、子表、三表),推荐写法, 让编译器自动推断 S 类型
*
* @param <T> 数据类型
*/
public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
public <S> MPJLambdaWrapperX<T> likeIfPresent(SFunction<S, ?> column, String val) {
if (StringUtils.hasText(val)) {
return (MPJLambdaWrapperX<T>) super.like(column, val);
}
return this;
}
public <S> MPJLambdaWrapperX<T> inIfPresent(SFunction<S, ?> column, Collection<?> values) {
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
return (MPJLambdaWrapperX<T>) super.in(column, values);
}
return this;
}
public <S> MPJLambdaWrapperX<T> inIfPresent(SFunction<S, ?> column, Object... values) {
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
return (MPJLambdaWrapperX<T>) super.in(column, values);
}
return this;
}
public <S> MPJLambdaWrapperX<T> eqIfPresent(SFunction<S, ?> column, Object val) {
if (ObjectUtil.isNotEmpty(val)) {
return (MPJLambdaWrapperX<T>) super.eq(column, val);
}
return this;
}
public <S> MPJLambdaWrapperX<T> neIfPresent(SFunction<S, ?> column, Object val) {
if (ObjectUtil.isNotEmpty(val)) {
return (MPJLambdaWrapperX<T>) super.ne(column, val);
}
return this;
}
public <S> MPJLambdaWrapperX<T> gtIfPresent(SFunction<S, ?> column, Object val) {
if (val != null) {
return (MPJLambdaWrapperX<T>) super.gt(column, val);
}
return this;
}
public <S> MPJLambdaWrapperX<T> geIfPresent(SFunction<S, ?> column, Object val) {
if (val != null) {
return (MPJLambdaWrapperX<T>) super.ge(column, val);
}
return this;
}
public <S> MPJLambdaWrapperX<T> ltIfPresent(SFunction<S, ?> column, Object val) {
if (val != null) {
return (MPJLambdaWrapperX<T>) super.lt(column, val);
}
return this;
}
public <S> MPJLambdaWrapperX<T> leIfPresent(SFunction<S, ?> column, Object val) {
if (val != null) {
return (MPJLambdaWrapperX<T>) super.le(column, val);
}
return this;
}
public <S> MPJLambdaWrapperX<T> betweenIfPresent(SFunction<S, ?> column, Object[] values) {
Object val1 = ArrayUtils.get(values, 0);
Object val2 = ArrayUtils.get(values, 1);
return betweenIfPresent(column, val1, val2);
}
public <S> MPJLambdaWrapperX<T> betweenIfPresent(SFunction<S, ?> column, Object val1, Object val2) {
if (val1 != null && val2 != null) {
return (MPJLambdaWrapperX<T>) super.between(column, val1, val2);
}
if (val1 != null) {
return (MPJLambdaWrapperX<T>) super.ge(column, val1);
}
if (val2 != null) {
return (MPJLambdaWrapperX<T>) super.le(column, val2);
}
return this;
}
// ========== 重写父类方法,方便链式调用 ==========
@Override
public <X> MPJLambdaWrapperX<T> eq(boolean condition, SFunction<X, ?> column, Object val) {
super.eq(condition, column, val);
return this;
}
@Override
public <X> MPJLambdaWrapperX<T> eq(SFunction<X, ?> column, Object val) {
super.eq(column, val);
return this;
}
@Override
public <X> MPJLambdaWrapperX<T> orderByDesc(SFunction<X, ?> column) {
super.orderByDesc(true, column);
return this;
}
@Override
public <X> MPJLambdaWrapperX<T> orderByAsc(SFunction<X, ?> column) {
super.orderByAsc(true, column);
return this;
}
@Override
public MPJLambdaWrapperX<T> last(String lastSql) {
super.last(lastSql);
return this;
}
@Override
public <X> MPJLambdaWrapperX<T> in(SFunction<X, ?> column, Collection<?> coll) {
super.in(column, coll);
return this;
}
@Override
public MPJLambdaWrapperX<T> selectAll(Class<?> clazz) {
super.selectAll(clazz);
return this;
}
@Override
public MPJLambdaWrapperX<T> selectAll(Class<?> clazz, String prefix) {
super.selectAll(clazz, prefix);
return this;
}
@Override
public <S> MPJLambdaWrapperX<T> selectAs(SFunction<S, ?> column, String alias) {
super.selectAs(column, alias);
return this;
}
@Override
public <E> MPJLambdaWrapperX<T> selectAs(String column, SFunction<E, ?> alias) {
super.selectAs(column, alias);
return this;
}
@Override
public <S, X> MPJLambdaWrapperX<T> selectAs(SFunction<S, ?> column, SFunction<X, ?> alias) {
super.selectAs(column, alias);
return this;
}
@Override
public <E, X> MPJLambdaWrapperX<T> selectAs(String index, SFunction<E, ?> column, SFunction<X, ?> alias) {
super.selectAs(index, column, alias);
return this;
}
@Override
public <E> MPJLambdaWrapperX<T> selectAsClass(Class<E> source, Class<?> tag) {
super.selectAsClass(source, tag);
return this;
}
@Override
public <E, F> MPJLambdaWrapperX<T> selectSub(Class<E> clazz, Consumer<MPJLambdaWrapper<E>> consumer, SFunction<F, ?> alias) {
super.selectSub(clazz, consumer, alias);
return this;
}
@Override
public <E, F> MPJLambdaWrapperX<T> selectSub(Class<E> clazz, String st, Consumer<MPJLambdaWrapper<E>> consumer, SFunction<F, ?> alias) {
super.selectSub(clazz, st, consumer, alias);
return this;
}
@Override
public <S> MPJLambdaWrapperX<T> selectCount(SFunction<S, ?> column) {
super.selectCount(column);
return this;
}
@Override
public MPJLambdaWrapperX<T> selectCount(Object column, String alias) {
super.selectCount(column, alias);
return this;
}
@Override
public <X> MPJLambdaWrapperX<T> selectCount(Object column, SFunction<X, ?> alias) {
super.selectCount(column, alias);
return this;
}
@Override
public <S> MPJLambdaWrapperX<T> selectCount(SFunction<S, ?> column, String alias) {
super.selectCount(column, alias);
return this;
}
@Override
public <S, X> MPJLambdaWrapperX<T> selectCount(SFunction<S, ?> column, SFunction<X, ?> alias) {
super.selectCount(column, alias);
return this;
}
@Override
public <S> MPJLambdaWrapperX<T> selectSum(SFunction<S, ?> column) {
super.selectSum(column);
return this;
}
@Override
public <S> MPJLambdaWrapperX<T> selectSum(SFunction<S, ?> column, String alias) {
super.selectSum(column, alias);
return this;
}
@Override
public <S, X> MPJLambdaWrapperX<T> selectSum(SFunction<S, ?> column, SFunction<X, ?> alias) {
super.selectSum(column, alias);
return this;
}
@Override
public <S> MPJLambdaWrapperX<T> selectMax(SFunction<S, ?> column) {
super.selectMax(column);
return this;
}
@Override
public <S> MPJLambdaWrapperX<T> selectMax(SFunction<S, ?> column, String alias) {
super.selectMax(column, alias);
return this;
}
@Override
public <S, X> MPJLambdaWrapperX<T> selectMax(SFunction<S, ?> column, SFunction<X, ?> alias) {
super.selectMax(column, alias);
return this;
}
@Override
public <S> MPJLambdaWrapperX<T> selectMin(SFunction<S, ?> column) {
super.selectMin(column);
return this;
}
@Override
public <S> MPJLambdaWrapperX<T> selectMin(SFunction<S, ?> column, String alias) {
super.selectMin(column, alias);
return this;
}
@Override
public <S, X> MPJLambdaWrapperX<T> selectMin(SFunction<S, ?> column, SFunction<X, ?> alias) {
super.selectMin(column, alias);
return this;
}
@Override
public <S> MPJLambdaWrapperX<T> selectAvg(SFunction<S, ?> column) {
super.selectAvg(column);
return this;
}
@Override
public <S> MPJLambdaWrapperX<T> selectAvg(SFunction<S, ?> column, String alias) {
super.selectAvg(column, alias);
return this;
}
@Override
public <S, X> MPJLambdaWrapperX<T> selectAvg(SFunction<S, ?> column, SFunction<X, ?> alias) {
super.selectAvg(column, alias);
return this;
}
@Override
public <S> MPJLambdaWrapperX<T> selectLen(SFunction<S, ?> column) {
super.selectLen(column);
return this;
}
@Override
public <S> MPJLambdaWrapperX<T> selectLen(SFunction<S, ?> column, String alias) {
super.selectLen(column, alias);
return this;
}
@Override
public <S, X> MPJLambdaWrapperX<T> selectLen(SFunction<S, ?> column, SFunction<X, ?> alias) {
super.selectLen(column, alias);
return this;
}
// ========== 关键重写:使 leftJoin 返回当前类型 this ==========
@Override
public <A, B> MPJLambdaWrapperX<T> leftJoin(Class<A> clazz, SFunction<A, ?> left, SFunction<B, ?> right) {
super.leftJoin(clazz, left, right);
return this;
}
@Override
public <A, B> MPJLambdaWrapperX<T> rightJoin(Class<A> clazz, SFunction<A, ?> left, SFunction<B, ?> right) {
super.rightJoin(clazz, left, right);
return this;
}
@Override
public <A, B> MPJLambdaWrapperX<T> innerJoin(Class<A> clazz, SFunction<A, ?> left, SFunction<B, ?> right) {
super.innerJoin(clazz, left, right);
return this;
}
// ========== 添加扩展 Join 支持 ext 函数式参数 ==========
public <A, B> MPJLambdaWrapperX<T> leftJoin(Class<A> clazz, SFunction<A, ?> left, SFunction<B, ?> right, Consumer<MPJLambdaWrapperX<T>> ext) {
super.leftJoin(clazz, left, right);
if (ext != null) ext.accept(this);
return this;
}
public <A, B> MPJLambdaWrapperX<T> rightJoin(Class<A> clazz, SFunction<A, ?> left, SFunction<B, ?> right, Consumer<MPJLambdaWrapperX<T>> ext) {
super.rightJoin(clazz, left, right);
if (ext != null) ext.accept(this);
return this;
}
public <A, B> MPJLambdaWrapperX<T> innerJoin(Class<A> clazz, SFunction<A, ?> left, SFunction<B, ?> right, Consumer<MPJLambdaWrapperX<T>> ext) {
super.innerJoin(clazz, left, right);
if (ext != null) ext.accept(this);
return this;
}
}

View File

@@ -0,0 +1,166 @@
package com.njcn.rdms.framework.mybatis.core.query;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.ArrayUtils;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.njcn.rdms.framework.mybatis.core.util.JdbcUtils;
import org.springframework.util.StringUtils;
import java.util.Collection;
/**
* 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能:
*
* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
*
* @param <T> 数据类型
*/
public class QueryWrapperX<T> extends QueryWrapper<T> {
public QueryWrapperX<T> likeIfPresent(String column, String val) {
if (StringUtils.hasText(val)) {
return (QueryWrapperX<T>) super.like(column, val);
}
return this;
}
public QueryWrapperX<T> inIfPresent(String column, Collection<?> values) {
if (!CollectionUtils.isEmpty(values)) {
return (QueryWrapperX<T>) super.in(column, values);
}
return this;
}
public QueryWrapperX<T> inIfPresent(String column, Object... values) {
if (!ArrayUtils.isEmpty(values)) {
return (QueryWrapperX<T>) super.in(column, values);
}
return this;
}
public QueryWrapperX<T> eqIfPresent(String column, Object val) {
if (val != null) {
return (QueryWrapperX<T>) super.eq(column, val);
}
return this;
}
public QueryWrapperX<T> neIfPresent(String column, Object val) {
if (val != null) {
return (QueryWrapperX<T>) super.ne(column, val);
}
return this;
}
public QueryWrapperX<T> gtIfPresent(String column, Object val) {
if (val != null) {
return (QueryWrapperX<T>) super.gt(column, val);
}
return this;
}
public QueryWrapperX<T> geIfPresent(String column, Object val) {
if (val != null) {
return (QueryWrapperX<T>) super.ge(column, val);
}
return this;
}
public QueryWrapperX<T> ltIfPresent(String column, Object val) {
if (val != null) {
return (QueryWrapperX<T>) super.lt(column, val);
}
return this;
}
public QueryWrapperX<T> leIfPresent(String column, Object val) {
if (val != null) {
return (QueryWrapperX<T>) super.le(column, val);
}
return this;
}
public QueryWrapperX<T> betweenIfPresent(String column, Object val1, Object val2) {
if (val1 != null && val2 != null) {
return (QueryWrapperX<T>) super.between(column, val1, val2);
}
if (val1 != null) {
return (QueryWrapperX<T>) ge(column, val1);
}
if (val2 != null) {
return (QueryWrapperX<T>) le(column, val2);
}
return this;
}
public QueryWrapperX<T> betweenIfPresent(String column, Object[] values) {
if (values!= null && values.length != 0 && values[0] != null && values[1] != null) {
return (QueryWrapperX<T>) super.between(column, values[0], values[1]);
}
if (values!= null && values.length != 0 && values[0] != null) {
return (QueryWrapperX<T>) ge(column, values[0]);
}
if (values!= null && values.length != 0 && values[1] != null) {
return (QueryWrapperX<T>) le(column, values[1]);
}
return this;
}
// ========== 重写父类方法,方便链式调用 ==========
@Override
public QueryWrapperX<T> eq(boolean condition, String column, Object val) {
super.eq(condition, column, val);
return this;
}
@Override
public QueryWrapperX<T> eq(String column, Object val) {
super.eq(column, val);
return this;
}
@Override
public QueryWrapperX<T> orderByDesc(String column) {
super.orderByDesc(true, column);
return this;
}
@Override
public QueryWrapperX<T> last(String lastSql) {
super.last(lastSql);
return this;
}
@Override
public QueryWrapperX<T> in(String column, Collection<?> coll) {
super.in(column, coll);
return this;
}
/**
* 设置只返回最后一条
*
* TODO 不是完美解,需要在思考下。如果使用多数据源,并且数据源是多种类型时,可能会存在问题:实现之返回一条的语法不同
*
* @return this
*/
public QueryWrapperX<T> limitN(int n) {
DbType dbType = JdbcUtils.getDbType();
switch (dbType) {
case ORACLE:
case ORACLE_12C:
super.le("ROWNUM", n);
break;
case SQL_SERVER:
case SQL_SERVER2005:
super.select("TOP " + n + " *"); // 由于 SQL Server 是通过 SELECT TOP 1 实现限制一条,所以只好使用 * 查询剩余字段
break;
default: // MySQL、PostgreSQL、DM 达梦、KingbaseES 大金都是采用 LIMIT 实现
super.last("LIMIT " + n);
}
return this;
}
}

View File

@@ -0,0 +1,75 @@
package com.njcn.rdms.framework.mybatis.core.type;
import cn.hutool.core.lang.Assert;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import cn.hutool.extra.spring.SpringUtil;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 字段字段的 TypeHandler 实现类,基于 {@link AES} 实现
* 可通过 jasypt.encryptor.password 配置项,设置密钥
*
* @author hongawen
*/
public class EncryptTypeHandler extends BaseTypeHandler<String> {
private static final String ENCRYPTOR_PROPERTY_NAME = "mybatis-plus.encryptor.password";
private static AES aes;
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, encrypt(parameter));
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
return decrypt(value);
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
return decrypt(value);
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
return decrypt(value);
}
private static String decrypt(String value) {
if (value == null) {
return null;
}
return getEncryptor().decryptStr(value);
}
public static String encrypt(String rawValue) {
if (rawValue == null) {
return null;
}
return getEncryptor().encryptBase64(rawValue);
}
private static AES getEncryptor() {
if (aes != null) {
return aes;
}
// 构建 AES
String password = SpringUtil.getProperty(ENCRYPTOR_PROPERTY_NAME);
Assert.notEmpty(password, "配置项({}) 不能为空", ENCRYPTOR_PROPERTY_NAME);
aes = SecureUtil.aes(password.getBytes());
return aes;
}
}

View File

@@ -0,0 +1,56 @@
package com.njcn.rdms.framework.mybatis.core.type;
import cn.hutool.core.collection.CollUtil;
import com.njcn.rdms.framework.common.util.string.StrUtils;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
* List<Integer> 的类型转换器实现类,对应数据库的 varchar 类型
*
* @author jason
*/
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(List.class)
public class IntegerListTypeHandler implements TypeHandler<List<Integer>> {
private static final String COMMA = ",";
@Override
public void setParameter(PreparedStatement ps, int i, List<Integer> strings, JdbcType jdbcType) throws SQLException {
ps.setString(i, CollUtil.join(strings, COMMA));
}
@Override
public List<Integer> getResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
return getResult(value);
}
@Override
public List<Integer> getResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
return getResult(value);
}
@Override
public List<Integer> getResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
return getResult(value);
}
private List<Integer> getResult(String value) {
if (value == null) {
return null;
}
return StrUtils.splitToInteger(value, COMMA);
}
}

View File

@@ -0,0 +1,57 @@
package com.njcn.rdms.framework.mybatis.core.type;
import cn.hutool.core.collection.CollUtil;
import com.njcn.rdms.framework.common.util.string.StrUtils;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
* List<Long> 的类型转换器实现类,对应数据库的 varchar 类型
*
* @author hongawen
*/
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(List.class)
public class LongListTypeHandler implements TypeHandler<List<Long>> {
private static final String COMMA = ",";
@Override
public void setParameter(PreparedStatement ps, int i, List<Long> strings, JdbcType jdbcType) throws SQLException {
// 设置占位符
ps.setString(i, CollUtil.join(strings, COMMA));
}
@Override
public List<Long> getResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
return getResult(value);
}
@Override
public List<Long> getResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
return getResult(value);
}
@Override
public List<Long> getResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
return getResult(value);
}
private List<Long> getResult(String value) {
if (value == null) {
return null;
}
return StrUtils.splitToLong(value, COMMA);
}
}

View File

@@ -0,0 +1,58 @@
package com.njcn.rdms.framework.mybatis.core.type;
import cn.hutool.core.collection.CollUtil;
import com.njcn.rdms.framework.common.util.string.StrUtils;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Set;
/**
* Set<Long> 的类型转换器实现类,对应数据库的 varchar 类型
*
* @author hongawen
*/
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(List.class)
public class LongSetTypeHandler implements TypeHandler<Set<Long>> {
private static final String COMMA = ",";
@Override
public void setParameter(PreparedStatement ps, int i, Set<Long> strings, JdbcType jdbcType) throws SQLException {
// 设置占位符
ps.setString(i, CollUtil.join(strings, COMMA));
}
@Override
public Set<Long> getResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
return getResult(value);
}
@Override
public Set<Long> getResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
return getResult(value);
}
@Override
public Set<Long> getResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
return getResult(value);
}
private Set<Long> getResult(String value) {
if (value == null) {
return null;
}
return StrUtils.splitToLongSet(value, COMMA);
}
}

View File

@@ -0,0 +1,58 @@
package com.njcn.rdms.framework.mybatis.core.type;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
* List<String> 的类型转换器实现类,对应数据库的 varchar 类型
*
* @author 永不言败
* @since 2022 3/23 12:50:15
*/
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(List.class)
public class StringListTypeHandler implements TypeHandler<List<String>> {
private static final String COMMA = ",";
@Override
public void setParameter(PreparedStatement ps, int i, List<String> strings, JdbcType jdbcType) throws SQLException {
// 设置占位符
ps.setString(i, CollUtil.join(strings, COMMA));
}
@Override
public List<String> getResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
return getResult(value);
}
@Override
public List<String> getResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
return getResult(value);
}
@Override
public List<String> getResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
return getResult(value);
}
private List<String> getResult(String value) {
if (value == null) {
return null;
}
return StrUtil.splitTrim(value, COMMA);
}
}

View File

@@ -0,0 +1,89 @@
package com.njcn.rdms.framework.mybatis.core.util;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.mybatisplus.annotation.DbType;
import com.njcn.rdms.framework.common.util.object.ObjectUtils;
import com.njcn.rdms.framework.common.util.spring.SpringUtils;
import com.njcn.rdms.framework.mybatis.core.enums.DbTypeEnum;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
/**
* JDBC 工具类
*
* @author hongawen
*/
public class JdbcUtils {
/**
* 判断连接是否正确
*
* @param url 数据源连接
* @param username 账号
* @param password 密码
* @return 是否正确
*/
public static boolean isConnectionOK(String url, String username, String password) {
try (Connection ignored = DriverManager.getConnection(url, username, password)) {
return true;
} catch (Exception ex) {
return false;
}
}
/**
* 获得 URL 对应的 DB 类型
*
* @param url URL
* @return DB 类型
*/
public static DbType getDbType(String url) {
return com.baomidou.mybatisplus.extension.toolkit.JdbcUtils.getDbType(url);
}
/**
* 通过当前数据库连接获得对应的 DB 类型
*
* @return DB 类型
*/
public static DbType getDbType() {
DataSource dataSource;
try {
DynamicRoutingDataSource dynamicRoutingDataSource = SpringUtils.getBean(DynamicRoutingDataSource.class);
dataSource = dynamicRoutingDataSource.determineDataSource();
} catch (NoSuchBeanDefinitionException e) {
dataSource = SpringUtils.getBean(DataSource.class);
}
try (Connection conn = dataSource.getConnection()) {
return DbTypeEnum.find(conn.getMetaData().getDatabaseProductName());
} catch (SQLException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
/**
* 判断 JDBC 连接是否为 SQLServer 数据库
*
* @param url JDBC 连接
* @return 是否为 SQLServer 数据库
*/
public static boolean isSQLServer(String url) {
DbType dbType = getDbType(url);
return isSQLServer(dbType);
}
/**
* 判断 JDBC 连接是否为 SQLServer 数据库
*
* @param dbType DB 类型
* @return 是否为 SQLServer 数据库
*/
public static boolean isSQLServer(DbType dbType) {
return ObjectUtils.equalsAny(dbType, DbType.SQL_SERVER, DbType.SQL_SERVER2005);
}
}

View File

@@ -0,0 +1,157 @@
package com.njcn.rdms.framework.mybatis.core.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.func.Func1;
import cn.hutool.core.lang.func.LambdaUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.rdms.framework.common.pojo.PageParam;
import com.njcn.rdms.framework.common.pojo.SortingField;
import com.njcn.rdms.framework.mybatis.core.enums.DbTypeEnum;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* MyBatis 工具类
*/
public class MyBatisUtils {
private static final String MYSQL_ESCAPE_CHARACTER = "`";
public static <T> Page<T> buildPage(PageParam pageParam) {
return buildPage(pageParam, null);
}
public static <T> Page<T> buildPage(PageParam pageParam, Collection<SortingField> sortingFields) {
// 页码 + 数量
Page<T> page = new Page<>(pageParam.getPageNo(), pageParam.getPageSize());
page.setOptimizeJoinOfCountSql(false); // 关联 issuehttps://gitee.com/zhijiantianya/rdms-cloud/issues/ID2QLL
// 排序字段
if (CollUtil.isNotEmpty(sortingFields)) {
for (SortingField sortingField : sortingFields) {
page.addOrder(new OrderItem().setAsc(SortingField.ORDER_ASC.equals(sortingField.getOrder()))
.setColumn(StrUtil.toUnderlineCase(sortingField.getField())));
}
}
return page;
}
@SuppressWarnings("PatternVariableCanBeUsed")
public static <T> void addOrder(Wrapper<T> wrapper, Collection<SortingField> sortingFields) {
if (CollUtil.isEmpty(sortingFields)) {
return;
}
if (wrapper instanceof QueryWrapper<T>) {
QueryWrapper<T> query = (QueryWrapper<T>) wrapper;
for (SortingField sortingField : sortingFields) {
query.orderBy(true,
SortingField.ORDER_ASC.equals(sortingField.getOrder()),
StrUtil.toUnderlineCase(sortingField.getField()));
}
} else if (wrapper instanceof LambdaQueryWrapper<T>) {
// LambdaQueryWrapper 不直接支持字符串字段排序,使用 last 方法拼接 ORDER BY
LambdaQueryWrapper<T> lambdaQuery = (LambdaQueryWrapper<T>) wrapper;
StringBuilder orderBy = new StringBuilder();
for (SortingField sortingField : sortingFields) {
if (StrUtil.isNotEmpty(orderBy)) {
orderBy.append(", ");
}
orderBy.append(StrUtil.toUnderlineCase(sortingField.getField()))
.append(" ")
.append(SortingField.ORDER_ASC.equals(sortingField.getOrder()) ? "ASC" : "DESC");
}
lambdaQuery.last("ORDER BY " + orderBy);
// 另外个思路https://blog.csdn.net/m0_59084856/article/details/138450913
} else {
throw new IllegalArgumentException("Unsupported wrapper type: " + wrapper.getClass().getName());
}
}
/**
* 将拦截器添加到链中
* 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置
*
* @param interceptor 链
* @param inner 拦截器
* @param index 位置
*/
public static void addInterceptor(MybatisPlusInterceptor interceptor, InnerInterceptor inner, int index) {
List<InnerInterceptor> inners = new ArrayList<>(interceptor.getInterceptors());
inners.add(index, inner);
interceptor.setInterceptors(inners);
}
/**
* 获得 Table 对应的表名
* <p>
* 兼容 MySQL 转义表名 `t_xxx`
*
* @param table 表
* @return 去除转移字符后的表名
*/
public static String getTableName(Table table) {
String tableName = table.getName();
if (tableName.startsWith(MYSQL_ESCAPE_CHARACTER) && tableName.endsWith(MYSQL_ESCAPE_CHARACTER)) {
tableName = tableName.substring(1, tableName.length() - 1);
}
return tableName;
}
/**
* 构建 Column 对象
*
* @param tableName 表名
* @param tableAlias 别名
* @param column 字段名
* @return Column 对象
*/
public static Column buildColumn(String tableName, Alias tableAlias, String column) {
if (tableAlias != null) {
tableName = tableAlias.getName();
}
return new Column(tableName + StringPool.DOT + column);
}
/**
* 跨数据库的 find_in_set 实现
*
* @param column 字段名称
* @param value 查询值(不带单引号)
* @return sql
*/
public static String findInSet(String column, Object value) {
DbType dbType = JdbcUtils.getDbType();
return DbTypeEnum.getFindInSetTemplate(dbType)
.replace("#{column}", column)
.replace("#{value}", StrUtil.toString(value));
}
/**
* 将驼峰命名转换为下划线命名
*
* 使用场景:
* 1. <a href="https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1357/files">fix:修复"商品统计聚合函数的别名与排序字段不符"导致的 SQL 异常</a>
*
* @param func 字段名函数(驼峰命名)
* @return 字段名(下划线命名)
*/
public static <T> String toUnderlineCase(Func1<T, ?> func) {
String fieldName = LambdaUtil.getFieldName(func);
return StrUtil.toUnderlineCase(fieldName);
}
}

View File

@@ -0,0 +1,4 @@
/**
* 使用 MyBatis Plus 提升使用 MyBatis 的开发效率
*/
package com.njcn.rdms.framework.mybatis;

View File

@@ -0,0 +1,18 @@
package com.njcn.rdms.framework.translate.config;
import com.fhs.trans.service.impl.TransService;
import com.njcn.rdms.framework.translate.core.TranslateUtils;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
public class RdmsTranslateAutoConfiguration {
@Bean
@SuppressWarnings({"InstantiationOfUtilityClass", "SpringJavaInjectionPointsAutowiringInspection"})
public TranslateUtils translateUtils(TransService transService) {
TranslateUtils.init(transService);
return new TranslateUtils();
}
}

View File

@@ -0,0 +1,37 @@
package com.njcn.rdms.framework.translate.core;
import cn.hutool.core.collection.CollUtil;
import com.fhs.core.trans.vo.VO;
import com.fhs.trans.service.impl.TransService;
import java.util.List;
/**
* VO 数据翻译 Utils
*
* @author hongawen
*/
public class TranslateUtils {
private static TransService transService;
public static void init(TransService transService) {
TranslateUtils.transService = transService;
}
/**
* 数据翻译
*
* 使用场景:无法使用 @TransMethodResult 注解的场景,只能通过手动触发翻译
*
* @param data 数据
* @return 翻译结果
*/
public static <T extends VO> List<T> translate(List<T> data) {
if (CollUtil.isNotEmpty((data))) {
transService.transBatch(data);
}
return data;
}
}

View File

@@ -0,0 +1,4 @@
/**
* 使用 Easy-Trans 提升使用 VO 数据翻译的开发效率
*/
package com.njcn.rdms.framework.translate;

View File

@@ -0,0 +1,2 @@
org.springframework.boot.env.EnvironmentPostProcessor=\
com.njcn.rdms.framework.mybatis.config.IdTypeEnvironmentPostProcessor

View File

@@ -0,0 +1,3 @@
com.njcn.rdms.framework.datasource.config.RdmsDataSourceAutoConfiguration
com.njcn.rdms.framework.mybatis.config.RdmsMybatisAutoConfiguration
com.njcn.rdms.framework.translate.config.RdmsTranslateAutoConfiguration