init: 导入RuoYi‑Vue‑Plus 6.X完整代码

This commit is contained in:
admin
2026-08-09 17:51:11 +08:00
commit 51cac459b3
914 changed files with 103833 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
<?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">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-mybatis</artifactId>
<description>
ruoyi-common-mybatis 数据库服务
</description>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<!-- satoken -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-satoken</artifactId>
</dependency>
<!-- api模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-api</artifactId>
</dependency>
<!-- dynamic-datasource 多数据源-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot4-starter</artifactId>
</dependency>
<!-- MyBatis Plus 启动器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot4-starter</artifactId>
</dependency>
<!-- MyBatis Plus JSqlParser 支持 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
</dependency>
<!-- MyBatis Plus Join -->
<dependency>
<groupId>com.github.yulichang</groupId>
<artifactId>mybatis-plus-join-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,220 @@
package com.github.yulichang.injector;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.AbstractSqlInjector;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import com.baomidou.mybatisplus.core.injector.ISqlInjector;
import com.baomidou.mybatisplus.core.injector.methods.SelectList;
import com.baomidou.mybatisplus.core.mapper.Mapper;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.toolkit.ArrayUtils;
import com.github.yulichang.base.JoinMapper;
import com.github.yulichang.method.*;
import com.github.yulichang.toolkit.MPJTableMapperHelper;
import com.github.yulichang.toolkit.ReflectionKit;
import com.github.yulichang.toolkit.TableHelper;
import lombok.Getter;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.session.Configuration;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
/**
* SQL 注入器
*
* @author yulichang
* @see DefaultSqlInjector
*/
@Getter
public class MPJSqlInjector extends DefaultSqlInjector {
/**
* 原始 SQL 注入器,用于兼容项目自定义注入逻辑。
*/
private AbstractSqlInjector sqlInjector;
/**
* 构造 MPJ SQL 注入器。
*/
public MPJSqlInjector() {
}
/**
* 构造带原始注入器的 MPJ SQL 注入器。
*
* @param sqlInjector 原始 SQL 注入器
*/
public MPJSqlInjector(ISqlInjector sqlInjector) {
if (Objects.nonNull(sqlInjector) && sqlInjector instanceof AbstractSqlInjector) {
this.sqlInjector = (AbstractSqlInjector) sqlInjector;
}
}
/**
* 获取 Mapper 可用的注入方法列表。
*
* @param configuration MyBatis 配置
* @param mapperClass Mapper 类型
* @param tableInfo 表信息
* @return 注入方法列表
*/
@Override
public List<AbstractMethod> getMethodList(Configuration configuration, Class<?> mapperClass, TableInfo tableInfo) {
if (!isJoinMapper(mapperClass)) {
if (Objects.nonNull(sqlInjector)) {
return sqlInjector.getMethodList(configuration, mapperClass, tableInfo);
}
return super.getMethodList(configuration, mapperClass, tableInfo);
}
if (Objects.nonNull(sqlInjector)) {
return methodFilter(sqlInjector.getMethodList(configuration, mapperClass, tableInfo));
}
return methodFilter(super.getMethodList(configuration, mapperClass, tableInfo));
}
/**
* 过滤并追加 MPJ 需要的 SQL 注入方法。
*
* @param list 原始注入方法列表
* @return 过滤后的注入方法列表
*/
private List<AbstractMethod> methodFilter(List<AbstractMethod> list) {
String packageStr = SelectList.class.getPackage().getName();
List<String> methodList = Arrays.asList(
"Update",
"Delete",
"SelectOne",
"SelectCount",
"SelectMaps",
"SelectMapsPage",
"SelectObjs",
"SelectList",
"SelectPage");
list.removeIf(i -> methodList.contains(i.getClass().getSimpleName()) &&
Objects.equals(packageStr, i.getClass().getPackage().getName()));
addAll(list, getWrapperMethod());
addAll(list, getJoinMethod());
return list;
}
/**
* 获取 MPJ 联表操作注入方法。
*
* @return 联表操作注入方法列表
*/
private List<AbstractMethod> getJoinMethod() {
List<AbstractMethod> list = new ArrayList<>();
list.add(new DeleteJoin(SqlMethod.DELETE_JOIN.getMethod()));
list.add(new UpdateJoin(SqlMethod.UPDATE_JOIN.getMethod()));
list.add(new UpdateJoinAndNull(SqlMethod.UPDATE_JOIN_AND_NULL.getMethod()));
list.add(new SelectJoinCount(SqlMethod.SELECT_JOIN_COUNT.getMethod()));
list.add(new SelectJoinOne(SqlMethod.SELECT_JOIN_ONE.getMethod()));
list.add(new SelectJoinList(SqlMethod.SELECT_JOIN_LIST.getMethod()));
list.add(new SelectJoinPage(SqlMethod.SELECT_JOIN_PAGE.getMethod()));
return list;
}
/**
* 获取 MPJ 覆盖 MyBatis-Plus 默认 Wrapper 的注入方法。
*
* @return Wrapper 注入方法列表
*/
private List<AbstractMethod> getWrapperMethod() {
List<AbstractMethod> list = new ArrayList<>();
list.add(new com.github.yulichang.method.mp.Delete());
list.add(new com.github.yulichang.method.mp.SelectOne());
list.add(new com.github.yulichang.method.mp.SelectCount());
list.add(new com.github.yulichang.method.mp.SelectMaps());
list.add(new com.github.yulichang.method.mp.SelectMapsPage());
list.add(new com.github.yulichang.method.mp.SelectObjs());
list.add(new com.github.yulichang.method.mp.SelectList());
list.add(new com.github.yulichang.method.mp.SelectPage());
list.add(new com.github.yulichang.method.mp.Update());
return list;
}
/**
* 将新增注入方法追加到原始列表中,已存在同名方法时不重复追加。
*
* @param source 原始方法列表
* @param addList 待追加方法列表
*/
private void addAll(List<AbstractMethod> source, List<AbstractMethod> addList) {
for (AbstractMethod method : addList) {
if (source.stream().noneMatch(m -> m.getClass().getSimpleName().equals(method.getClass().getSimpleName()))) {
source.add(method);
}
}
}
/**
* 初始化 Mapper 注入信息,并为 JoinMapper 注册 MPJ 表映射缓存。
*
* @param builderAssistant Mapper 构建助手
* @param mapperClass Mapper 类型
*/
@Override
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
super.inspectInject(builderAssistant, mapperClass);
if (!isJoinMapper(mapperClass)) {
return;
}
Class<?> modelClass = ReflectionKit.getSuperClassGenericType(mapperClass, Mapper.class, 0);
MPJTableMapperHelper.init(modelClass, mapperClass);
Supplier<Class<?>> supplier = () -> {
try {
return extractModelClassOld(mapperClass);
} catch (Throwable throwable) {
return null;
}
};
TableHelper.init(modelClass, supplier.get());
}
/**
* 兼容旧版泛型解析逻辑,提取 Mapper 绑定的实体类型。
*
* @param mapperClass Mapper 类型
* @return Mapper 泛型中的实体类型,无法解析时返回 null
*/
@SuppressWarnings("IfStatementWithIdenticalBranches")
protected Class<?> extractModelClassOld(Class<?> mapperClass) {
Type[] types = mapperClass.getGenericInterfaces();
ParameterizedType target = null;
for (Type type : types) {
if (type instanceof ParameterizedType) {
Type[] typeArray = ((ParameterizedType) type).getActualTypeArguments();
if (ArrayUtils.isNotEmpty(typeArray)) {
for (Type t : typeArray) {
if (t instanceof TypeVariable || t instanceof WildcardType) {
break;
} else {
target = (ParameterizedType) type;
break;
}
}
}
break;
}
}
return target == null ? null : (Class<?>) target.getActualTypeArguments()[0];
}
/**
* 判断 Mapper 是否继承 MPJ JoinMapper。
*
* @param mapperClass Mapper 类型
* @return true 是 JoinMapper false 不是 JoinMapper
*/
private boolean isJoinMapper(Class<?> mapperClass) {
return JoinMapper.class.isAssignableFrom(mapperClass);
}
}
@@ -0,0 +1,33 @@
package org.dromara.common.mybatis.annotation;
import java.lang.annotation.*;
/**
* 数据权限注解,用于标记数据权限的占位符关键字和替换值
* <p>
* 一个注解只能对应一个模板
* </p>
*
* @author Lion Li
* @version 3.5.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataColumn {
/**
* 数据权限模板的占位符关键字,默认为 "deptName"
*
* @return 占位符关键字数组
*/
String[] key() default "deptName";
/**
* 数据权限模板的占位符替换值,默认为 "dept_id"
*
* @return 占位符替换值数组
*/
String[] value() default "dept_id";
}
@@ -0,0 +1,30 @@
package org.dromara.common.mybatis.annotation;
import java.lang.annotation.*;
/**
* 数据权限组注解,用于标记数据权限配置数组
*
* @author Lion Li
* @version 3.5.0
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataPermission {
/**
* 数据权限配置数组,用于指定数据权限的占位符关键字和替换值
*
* @return 数据权限配置数组
*/
DataColumn[] value();
/**
* 权限拼接标识符(用于指定连接语句的sql符号)
* 如不填 默认 select 用 OR 其他语句用 AND
* 内容 OR 或者 AND
*/
String joinStr() default "";
}
@@ -0,0 +1,77 @@
package org.dromara.common.mybatis.aspect;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.dromara.common.mybatis.annotation.DataPermission;
import org.dromara.common.mybatis.helper.DataPermissionHelper;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 数据权限注解Advice
*
* @author 秋辞未寒
*/
public class DataPermissionAdvice implements MethodInterceptor {
/**
* 拦截带有数据权限注解的方法调用,设置当前线程的数据权限上下文。
*
* @param invocation 方法调用上下文
* @return 代理方法执行结果
* @throws Throwable 代理方法执行异常
*/
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Object target = invocation.getThis();
Method method = invocation.getMethod();
// 设置权限注解
DataPermissionHelper.setPermission(getDataPermissionAnnotation(target, method));
try {
// 执行代理方法
return invocation.proceed();
} finally {
// 清除权限注解
DataPermissionHelper.removePermission();
}
}
/**
* 获取数据权限注解
*
* @param target 目标对象
* @param method 当前执行方法
* @return 数据权限注解,未配置时返回 null
*/
private DataPermission getDataPermissionAnnotation(Object target, Method method) {
DataPermission dataPermission = method.getAnnotation(DataPermission.class);
// 优先获取方法上的注解
if (dataPermission != null) {
return dataPermission;
}
// 方法上没有注解,则获取类上的注解
Class<?> targetClass = target.getClass();
// 如果是 JDK 动态代理,则获取真实的Class实例
if (Proxy.isProxyClass(targetClass)) {
return getProxyClassDataPermission(targetClass);
}
return targetClass.getAnnotation(DataPermission.class);
}
/**
* 从 JDK 动态代理接口上获取数据权限注解。
*
* @param targetClass 代理类
* @return 数据权限注解,未配置时返回 null
*/
private DataPermission getProxyClassDataPermission(Class<?> targetClass) {
for (Class<?> interfaceClass : targetClass.getInterfaces()) {
DataPermission dataPermission = interfaceClass.getAnnotation(DataPermission.class);
if (dataPermission != null) {
return dataPermission;
}
}
return null;
}
}
@@ -0,0 +1,57 @@
package org.dromara.common.mybatis.aspect;
import org.dromara.common.mybatis.annotation.DataPermission;
import org.springframework.aop.support.StaticMethodMatcherPointcut;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 数据权限匹配切点
*
* @author 秋辞未寒
*/
public class DataPermissionPointcut extends StaticMethodMatcherPointcut {
/**
* 判断当前方法或目标类型是否命中数据权限切点。
*
* @param method 当前执行方法
* @param targetClass 目标类型
* @return true 命中数据权限切点 false 未命中
*/
@Override
public boolean matches(Method method, Class<?> targetClass) {
// 优先匹配方法
// 数据权限注解不对继承生效,所以检查当前方法是否有注解即可,不再往上匹配父类或接口
if (method.isAnnotationPresent(DataPermission.class)) {
return true;
}
// MyBatis 的 Mapper 就是通过 JDK 动态代理实现的,所以这里需要检查是否匹配 JDK 的动态代理
Class<?> targetClassRef = resolveTargetClass(targetClass);
return targetClassRef.isAnnotationPresent(DataPermission.class);
}
/**
* 解析真实目标类型,兼容 MyBatis Mapper 的 JDK 动态代理类。
*
* @param targetClass Spring AOP 传入的目标类型
* @return 真实目标类型或可匹配数据权限注解的接口类型
*/
private Class<?> resolveTargetClass(Class<?> targetClass) {
if (!Proxy.isProxyClass(targetClass)) {
return targetClass;
}
for (Class<?> interfaceClass : targetClass.getInterfaces()) {
// 数据权限注解不对继承生效,但由于 SpringIOC 容器拿到的实际上是 MyBatis 代理过后的 Mapper,而 targetClass.isAnnotationPresent 实际匹配的是 Proxy 类的注解,不会查找代理类。
// 所以这里不能用 targetClass.isAnnotationPresent,只能用 AnnotatedElementUtils.hasAnnotation 或 targetClass.getInterfaces()[0].isAnnotationPresent 去做匹配,以检查被代理的 MapperClass 是否具有注解
// 原理:JDK 动态代理本质上就是对接口进行实现然后对具体的接口实现做代理,所以直接通过接口可以拿到实际的 MapperClass
if (interfaceClass.isAnnotationPresent(DataPermission.class)) {
return interfaceClass;
}
}
return targetClass;
}
}
@@ -0,0 +1,53 @@
package org.dromara.common.mybatis.aspect;
import org.aopalliance.aop.Advice;
import org.springframework.aop.Pointcut;
import org.springframework.aop.support.AbstractPointcutAdvisor;
/**
* 数据权限注解切面定义
*
* @author 秋辞未寒
*/
@SuppressWarnings("all")
public class DataPermissionPointcutAdvisor extends AbstractPointcutAdvisor {
/**
* 数据权限通知逻辑。
*/
private final Advice advice;
/**
* 数据权限切点匹配器。
*/
private final Pointcut pointcut;
/**
* 构造数据权限切面定义。
*/
public DataPermissionPointcutAdvisor() {
this.advice = new DataPermissionAdvice();
this.pointcut = new DataPermissionPointcut();
}
/**
* 获取数据权限切点。
*
* @return 数据权限切点
*/
@Override
public Pointcut getPointcut() {
return this.pointcut;
}
/**
* 获取数据权限通知。
*
* @return 数据权限通知
*/
@Override
public Advice getAdvice() {
return this.advice;
}
}
@@ -0,0 +1,152 @@
package org.dromara.common.mybatis.config;
import cn.hutool.core.net.NetUtil;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.core.handlers.PostInitTableInfoHandler;
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.dromara.common.core.factory.YmlPropertySourceFactory;
import org.dromara.common.mybatis.aspect.DataPermissionPointcutAdvisor;
import org.dromara.common.mybatis.config.properties.SqlLogProperties;
import org.dromara.common.mybatis.handler.InjectionMetaObjectHandler;
import org.dromara.common.mybatis.handler.MybatisExceptionHandler;
import org.dromara.common.mybatis.handler.PlusPostInitTableInfoHandler;
import org.dromara.common.mybatis.interceptor.PlusDataPermissionInterceptor;
import org.dromara.common.mybatis.interceptor.SqlLogInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.annotation.Role;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* mybatis-plus配置类(下方注释有插件介绍)
*
* @author Lion Li
*/
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@EnableTransactionManagement(proxyTargetClass = true)
@MapperScan("${mybatis-plus.mapperPackage}")
@PropertySource(value = "classpath:common-mybatis.yml", factory = YmlPropertySourceFactory.class)
@EnableConfigurationProperties(SqlLogProperties.class)
public class MybatisPlusConfig {
/**
* 组装 MyBatis-Plus 核心拦截器链。
*
* @return MyBatis-Plus 拦截器
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 数据权限处理
interceptor.addInnerInterceptor(dataPermissionInterceptor());
// 分页插件
interceptor.addInnerInterceptor(paginationInnerInterceptor());
// 乐观锁插件
interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());
return interceptor;
}
/**
* 数据权限拦截器
*/
public PlusDataPermissionInterceptor dataPermissionInterceptor() {
return new PlusDataPermissionInterceptor();
}
/**
* 数据权限切面处理器
*/
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public DataPermissionPointcutAdvisor dataPermissionPointcutAdvisor() {
return new DataPermissionPointcutAdvisor();
}
/**
* 分页插件,自动识别数据库类型
*/
public PaginationInnerInterceptor paginationInnerInterceptor() {
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
// 分页合理化
paginationInnerInterceptor.setOverflow(true);
return paginationInnerInterceptor;
}
/**
* 乐观锁插件
*/
public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor() {
return new OptimisticLockerInnerInterceptor();
}
/**
* 完整 SQL 日志拦截器
*/
@Bean
@ConditionalOnProperty(prefix = "mybatis-plus.sql-log", name = "enabled", havingValue = "true")
public SqlLogInterceptor sqlLogInterceptor(SqlLogProperties sqlLogProperties) {
return new SqlLogInterceptor(sqlLogProperties);
}
/**
* 元对象字段填充控制器
*/
@Bean
public MetaObjectHandler metaObjectHandler() {
return new InjectionMetaObjectHandler();
}
/**
* 使用网卡信息绑定雪花生成器
* 防止集群雪花ID重复
*/
@Bean
public IdentifierGenerator idGenerator() {
return new DefaultIdentifierGenerator(NetUtil.getLocalhost());
}
/**
* 异常处理器
*/
@Bean
public MybatisExceptionHandler mybatisExceptionHandler() {
return new MybatisExceptionHandler();
}
/**
* 初始化表对象处理器
*/
@Bean
public PostInitTableInfoHandler postInitTableInfoHandler() {
return new PlusPostInitTableInfoHandler();
}
/**
* PaginationInnerInterceptor 分页插件,自动识别数据库类型
* https://baomidou.com/pages/97710a/
* OptimisticLockerInnerInterceptor 乐观锁插件
* https://baomidou.com/pages/0d93c0/
* MetaObjectHandler 元对象字段填充控制器
* https://baomidou.com/pages/4c6bcf/
* ISqlInjector sql注入器
* https://baomidou.com/pages/42ea4a/
* BlockAttackInnerInterceptor 如果是对全表的删除或更新操作,就会终止该操作
* https://baomidou.com/pages/f9a237/
* IllegalSQLInnerInterceptor sql性能规范插件(垃圾SQL拦截)
* IdentifierGenerator 自定义主键策略
* https://baomidou.com/pages/568eb2/
* TenantLineInnerInterceptor 多租户插件
* https://baomidou.com/pages/aef2f2/
* DynamicTableNameInnerInterceptor 动态表名插件
* https://baomidou.com/pages/2a45ff/
*/
}
@@ -0,0 +1,25 @@
package org.dromara.common.mybatis.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* SQL 日志配置。
*
* @author Lion Li
*/
@Data
@ConfigurationProperties(prefix = "mybatis-plus.sql-log")
public class SqlLogProperties {
/**
* 是否开启完整 SQL 输出。
*/
private Boolean enabled = false;
/**
* 输出方式,可选 console、log。
*/
private String output = "console";
}
@@ -0,0 +1,52 @@
package org.dromara.common.mybatis.core.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* Entity基类
*
* @author Lion Li
*/
@Data
public class BaseEntity implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 创建部门
*/
@TableField(fill = FieldFill.INSERT)
private Long createDept;
/**
* 创建者
*/
@TableField(fill = FieldFill.INSERT)
private Long createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新者
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateBy;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
@@ -0,0 +1,34 @@
package org.dromara.common.mybatis.core.domain;
import cn.hutool.core.collection.CollUtil;
import java.io.Serial;
import java.io.Serializable;
import java.util.Set;
/**
* 当前请求的数据权限访问上下文
*
* @param perms 当前请求接口权限标识集合
* @param roleKeys 当前请求角色标识集合
* @author Lion Li
*/
public record DataPermissionAccess(Set<String> perms, Set<String> roleKeys) implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 空访问上下文,表示不按接口权限或角色约束数据权限角色。
*/
public static final DataPermissionAccess EMPTY = new DataPermissionAccess(Set.of(), Set.of());
/**
* 是否存在数据权限约束。
*
* @return true 存在权限约束 false 不存在权限约束
*/
public boolean constrained() {
return CollUtil.isNotEmpty(perms) || CollUtil.isNotEmpty(roleKeys);
}
}
@@ -0,0 +1,386 @@
package org.dromara.common.mybatis.core.mapper;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
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.reflect.GenericTypeUtils;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers;
import com.baomidou.mybatisplus.extension.toolkit.Db;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StreamUtils;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
/**
* 自定义 Mapper 接口, 实现 自定义扩展
*
* @param <T> table 泛型
* @param <V> vo 泛型
* @author Lion Li
* @since 2021-05-13
*/
@SuppressWarnings("unchecked")
public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
/**
* Mapper 日志对象。
*/
Log log = LogFactory.getLog(BaseMapperPlus.class);
/**
* Mapper 泛型类型缓存,避免重复解析实体与 VO 类型。
*/
ClassValue<Class<?>[]> TYPE_ARGUMENT_CACHE = new ClassValue<>() {
/**
* 解析指定 Mapper 类型的实体与 VO 泛型。
*
* @param type Mapper 类型
* @return 泛型类型数组
*/
@Override
protected Class<?>[] computeValue(Class<?> type) {
return GenericTypeUtils.resolveTypeArguments(type, BaseMapperPlus.class);
}
};
/**
* 获取当前实例对象关联的泛型类型 V 的 Class 对象
*
* @return 返回当前实例对象关联的泛型类型 V 的 Class 对象
*/
default Class<V> currentVoClass() {
return (Class<V>) currentMapperTypes()[1];
}
/**
* 获取当前实例对象关联的泛型类型 T 的 Class 对象
*
* @return 返回当前实例对象关联的泛型类型 T 的 Class 对象
*/
default Class<T> currentModelClass() {
return (Class<T>) currentMapperTypes()[0];
}
/**
* 获取当前 Mapper 的实体与 VO 泛型类型。
*
* @return 泛型类型数组
*/
private Class<?>[] currentMapperTypes() {
Class<?>[] types = TYPE_ARGUMENT_CACHE.get(this.getClass());
if (types == null || types.length < 2) {
throw new IllegalStateException("无法解析 Mapper 泛型类型: " + this.getClass().getName());
}
return types;
}
/**
* 使用默认的查询条件查询并返回结果列表
*
* @return 返回查询结果的列表
*/
default List<T> selectList() {
return this.selectList(new QueryWrapper<>());
}
/**
* 创建当前 Mapper 绑定的 Lambda CRUD 链式操作。
*
* @return Lambda CRUD 链式包装器
*/
default LambdaCrudChainWrapper<T, V> lambda() {
return new LambdaCrudChainWrapper<>(this);
}
/**
* 创建当前 Mapper 绑定的 Lambda 链式更新。
*
* @return Lambda 链式更新包装器
*/
default LambdaUpdateChainWrapper<T> lambdaUpdate() {
return ChainWrappers.lambdaUpdateChain(this);
}
/**
* 批量插入实体对象集合
*
* @param entityList 实体对象集合
* @return 插入操作是否成功的布尔值
*/
default boolean insertBatch(Collection<T> entityList) {
return Db.saveBatch(entityList);
}
/**
* 批量根据ID更新实体对象集合
*
* @param entityList 实体对象集合
* @return 更新操作是否成功的布尔值
*/
default boolean updateBatchById(Collection<T> entityList) {
return Db.updateBatchById(entityList);
}
/**
* 批量插入或更新实体对象集合
*
* @param entityList 实体对象集合
* @return 插入或更新操作是否成功的布尔值
*/
default boolean insertOrUpdateBatch(Collection<T> entityList) {
return Db.saveOrUpdateBatch(entityList);
}
/**
* 批量插入实体对象集合并指定批处理大小
*
* @param entityList 实体对象集合
* @param batchSize 批处理大小
* @return 插入操作是否成功的布尔值
*/
default boolean insertBatch(Collection<T> entityList, int batchSize) {
return Db.saveBatch(entityList, batchSize);
}
/**
* 批量根据ID更新实体对象集合并指定批处理大小
*
* @param entityList 实体对象集合
* @param batchSize 批处理大小
* @return 更新操作是否成功的布尔值
*/
default boolean updateBatchById(Collection<T> entityList, int batchSize) {
return Db.updateBatchById(entityList, batchSize);
}
/**
* 批量插入或更新实体对象集合并指定批处理大小
*
* @param entityList 实体对象集合
* @param batchSize 批处理大小
* @return 插入或更新操作是否成功的布尔值
*/
default boolean insertOrUpdateBatch(Collection<T> entityList, int batchSize) {
return Db.saveOrUpdateBatch(entityList, batchSize);
}
/**
* 根据ID查询单个VO对象
*
* @param id 主键ID
* @return 查询到的单个VO对象
*/
default V selectVoById(Serializable id) {
return selectVoById(id, this.currentVoClass());
}
/**
* 根据ID查询单个VO对象并将其转换为指定的VO类
*
* @param id 主键ID
* @param voClass 要转换的VO类的Class对象
* @param <C> VO类的类型
* @return 查询到的单个VO对象,经过转换为指定的VO类后返回
*/
default <C> C selectVoById(Serializable id, Class<C> voClass) {
T obj = this.selectById(id);
if (ObjectUtil.isNull(obj)) {
return null;
}
return MapstructUtils.convert(obj, voClass);
}
/**
* 根据ID集合批量查询VO对象列表
*
* @param idList 主键ID集合
* @return 查询到的VO对象列表
*/
default List<V> selectVoByIds(Collection<? extends Serializable> idList) {
return selectVoByIds(idList, this.currentVoClass());
}
/**
* 根据ID集合批量查询实体对象列表,并将其转换为指定的VO对象列表
*
* @param idList 主键ID集合
* @param voClass 要转换的VO类的Class对象
* @param <C> VO类的类型
* @return 查询到的VO对象列表,经过转换为指定的VO类后返回
*/
default <C> List<C> selectVoByIds(Collection<? extends Serializable> idList, Class<C> voClass) {
List<T> list = this.selectByIds(idList);
if (CollUtil.isEmpty(list)) {
return CollUtil.newArrayList();
}
return MapstructUtils.convert(list, voClass);
}
/**
* 根据查询条件Map查询VO对象列表
*
* @param map 查询条件Map
* @return 查询到的VO对象列表
*/
default List<V> selectVoByMap(Map<String, Object> map) {
return selectVoByMap(map, this.currentVoClass());
}
/**
* 根据查询条件Map查询实体对象列表,并将其转换为指定的VO对象列表
*
* @param map 查询条件Map
* @param voClass 要转换的VO类的Class对象
* @param <C> VO类的类型
* @return 查询到的VO对象列表,经过转换为指定的VO类后返回
*/
default <C> List<C> selectVoByMap(Map<String, Object> map, Class<C> voClass) {
List<T> list = this.selectByMap(map);
if (CollUtil.isEmpty(list)) {
return CollUtil.newArrayList();
}
return MapstructUtils.convert(list, voClass);
}
/**
* 根据条件查询单个VO对象
*
* @param wrapper 查询条件Wrapper
* @return 查询到的单个VO对象
*/
default V selectVoOne(Wrapper<T> wrapper) {
return selectVoOne(wrapper, this.currentVoClass());
}
/**
* 根据条件查询单个VO对象,并根据需要决定是否抛出异常
*
* @param wrapper 查询条件Wrapper
* @param throwEx 是否抛出异常的标志
* @return 查询到的单个VO对象
*/
default V selectVoOne(Wrapper<T> wrapper, boolean throwEx) {
return selectVoOne(wrapper, this.currentVoClass(), throwEx);
}
/**
* 根据条件查询单个VO对象,并指定返回的VO对象的类型
*
* @param wrapper 查询条件Wrapper
* @param voClass 返回的VO对象的Class对象
* @param <C> 返回的VO对象的类型
* @return 查询到的单个VO对象,经过类型转换为指定的VO类后返回
*/
default <C> C selectVoOne(Wrapper<T> wrapper, Class<C> voClass) {
return selectVoOne(wrapper, voClass, true);
}
/**
* 根据条件查询单个实体对象,并将其转换为指定的VO对象
*
* @param wrapper 查询条件Wrapper
* @param voClass 要转换的VO类的Class对象
* @param throwEx 是否抛出异常的标志
* @param <C> VO类的类型
* @return 查询到的单个VO对象,经过转换为指定的VO类后返回
*/
default <C> C selectVoOne(Wrapper<T> wrapper, Class<C> voClass, boolean throwEx) {
T obj = this.selectOne(wrapper, throwEx);
if (ObjectUtil.isNull(obj)) {
return null;
}
return MapstructUtils.convert(obj, voClass);
}
/**
* 查询所有VO对象列表
*
* @return 查询到的VO对象列表
*/
default List<V> selectVoList() {
return selectVoList(new QueryWrapper<>(), this.currentVoClass());
}
/**
* 根据条件查询VO对象列表
*
* @param wrapper 查询条件Wrapper
* @return 查询到的VO对象列表
*/
default List<V> selectVoList(Wrapper<T> wrapper) {
return selectVoList(wrapper, this.currentVoClass());
}
/**
* 根据条件查询实体对象列表,并将其转换为指定的VO对象列表
*
* @param wrapper 查询条件Wrapper
* @param voClass 要转换的VO类的Class对象
* @param <C> VO类的类型
* @return 查询到的VO对象列表,经过转换为指定的VO类后返回
*/
default <C> List<C> selectVoList(Wrapper<T> wrapper, Class<C> voClass) {
List<T> list = this.selectList(wrapper);
if (CollUtil.isEmpty(list)) {
return CollUtil.newArrayList();
}
return MapstructUtils.convert(list, voClass);
}
/**
* 根据条件分页查询VO对象列表
*
* @param page 分页信息
* @param wrapper 查询条件Wrapper
* @return 查询到的VO对象分页列表
*/
default <P extends IPage<V>> P selectVoPage(IPage<T> page, Wrapper<T> wrapper) {
return selectVoPage(page, wrapper, this.currentVoClass());
}
/**
* 根据条件分页查询实体对象列表,并将其转换为指定的VO对象分页列表
*
* @param page 分页信息
* @param wrapper 查询条件Wrapper
* @param voClass 要转换的VO类的Class对象
* @param <C> VO类的类型
* @param <P> VO对象分页列表的类型
* @return 查询到的VO对象分页列表,经过转换为指定的VO类后返回
*/
default <C, P extends IPage<C>> P selectVoPage(IPage<T> page, Wrapper<T> wrapper, Class<C> voClass) {
// 根据条件分页查询实体对象列表
List<T> list = this.selectList(page, wrapper);
// 创建一个新的VO对象分页列表,并设置分页信息
IPage<C> voPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
if (CollUtil.isEmpty(list)) {
return (P) voPage;
}
voPage.setRecords(MapstructUtils.convert(list, voClass));
return (P) voPage;
}
/**
* 根据条件查询符合条件的对象,并将其转换为指定类型的对象列表
*
* @param wrapper 查询条件Wrapper
* @param mapper 转换函数,用于将查询到的对象转换为指定类型的对象
* @param <C> 要转换的对象的类型
* @return 查询到的符合条件的对象列表,经过转换为指定类型的对象后返回
*/
default <C> List<C> selectObjs(Wrapper<T> wrapper, Function<? super Object, C> mapper) {
return StreamUtils.toList(this.selectObjs(wrapper), mapper);
}
}
@@ -0,0 +1,909 @@
package org.dromara.common.mybatis.core.mapper;
import com.baomidou.mybatisplus.core.conditions.AbstractLambdaWrapper;
import com.baomidou.mybatisplus.core.conditions.SharedString;
import com.baomidou.mybatisplus.core.conditions.query.Query;
import com.baomidou.mybatisplus.core.conditions.segments.MergeSegments;
import com.baomidou.mybatisplus.core.conditions.update.Update;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.Assert;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import org.dromara.common.mybatis.core.query.AggregateSelectUtils;
import org.dromara.common.mybatis.core.query.LambdaQueryCondition;
import org.dromara.common.mybatis.core.query.SqlAggregateFunction;
import org.dromara.common.mybatis.core.query.SubQuery;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* Mapper 级 Lambda CRUD 链式包装器。
*
* @param <T> table 泛型
* @param <V> vo 泛型
* @author Lion Li
*/
public class LambdaCrudChainWrapper<T, V> extends AbstractLambdaWrapper<T, LambdaCrudChainWrapper<T, V>>
implements Query<LambdaCrudChainWrapper<T, V>, T, SFunction<T, ?>>,
Update<LambdaCrudChainWrapper<T, V>, SFunction<T, ?>>,
LambdaQueryCondition<T, LambdaCrudChainWrapper<T, V>> {
/**
* 当前链式操作绑定的 Mapper。
*/
private final BaseMapperPlus<T, V> crudMapper;
/**
* 更新 SET 片段集合。
*/
private final List<String> sqlSet;
/**
* 查询字段 SQL 片段。
*/
private SharedString sqlSelect = new SharedString();
/**
* 构造 Mapper 级 Lambda CRUD 链式包装器。
*
* @param crudMapper Mapper 对象
*/
public LambdaCrudChainWrapper(BaseMapperPlus<T, V> crudMapper) {
this.crudMapper = crudMapper;
super.setEntityClass(crudMapper.currentModelClass());
super.initNeed();
this.sqlSet = new ArrayList<>();
}
/**
* 构造 Mapper 级 Lambda CRUD 链式包装器实例。
*
* @param crudMapper Mapper 对象
* @param entity 实体对象
* @param entityClass 实体类型
* @param sqlSelect 查询字段 SQL 片段
* @param sqlSet 更新 set SQL 片段集合
* @param paramNameSeq 参数名称序列
* @param paramNameValuePairs 参数名称与参数值映射
* @param mergeSegments 查询条件表达式
* @param paramAlias 参数别名
* @param lastSql SQL 尾部片段
* @param sqlComment SQL 注释片段
* @param sqlFirst SQL 起始片段
*/
LambdaCrudChainWrapper(BaseMapperPlus<T, V> crudMapper, T entity, Class<T> entityClass, SharedString sqlSelect,
List<String> sqlSet, AtomicInteger paramNameSeq, Map<String, Object> paramNameValuePairs,
MergeSegments mergeSegments, SharedString paramAlias, SharedString lastSql,
SharedString sqlComment, SharedString sqlFirst) {
this.crudMapper = crudMapper;
super.setEntity(entity);
super.setEntityClass(entityClass);
this.sqlSelect = sqlSelect == null ? new SharedString() : sqlSelect;
this.sqlSet = sqlSet == null ? new ArrayList<>() : sqlSet;
this.paramNameSeq = paramNameSeq;
this.paramNameValuePairs = paramNameValuePairs;
this.expression = mergeSegments;
this.paramAlias = paramAlias;
this.lastSql = lastSql;
this.sqlComment = sqlComment;
this.sqlFirst = sqlFirst;
}
/**
* 按条件选择查询字段。
*
* @param condition 是否选择字段
* @param columns 查询字段集合
* @return this
*/
@Override
public LambdaCrudChainWrapper<T, V> select(boolean condition, List<SFunction<T, ?>> columns) {
if (condition && CollectionUtils.isNotEmpty(columns)) {
this.sqlSelect.setStringValue(columnsToString(false, columns));
}
return typedThis;
}
/**
* 选择查询字段。
*
* @param columns 查询字段
* @return this
*/
@Override
@SafeVarargs
public final LambdaCrudChainWrapper<T, V> select(SFunction<T, ?>... columns) {
return select(true, CollectionUtils.toList(columns));
}
/**
* 按条件选择查询字段。
*
* @param condition 是否选择字段
* @param columns 查询字段
* @return this
*/
@Override
@SafeVarargs
public final LambdaCrudChainWrapper<T, V> select(boolean condition, SFunction<T, ?>... columns) {
return select(condition, CollectionUtils.toList(columns));
}
/**
* 指定 SUM 聚合查询字段。
*
* @param column 聚合字段
* @return this
*/
public LambdaCrudChainWrapper<T, V> selectSum(SFunction<T, ?> column) {
return selectSum(column, AggregateSelectUtils.aliasName(column));
}
/**
* 指定 SUM 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名
* @return this
*/
public LambdaCrudChainWrapper<T, V> selectSum(SFunction<T, ?> column, String alias) {
return selectAggregate(SqlAggregateFunction.SUM, column, alias);
}
/**
* 指定 SUM 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名字段
* @param <A> 查询结果类型
* @return this
*/
public <A> LambdaCrudChainWrapper<T, V> selectSum(SFunction<T, ?> column, SFunction<A, ?> alias) {
return selectSum(column, AggregateSelectUtils.aliasName(alias));
}
/**
* 指定 MAX 聚合查询字段。
*
* @param column 聚合字段
* @return this
*/
public LambdaCrudChainWrapper<T, V> selectMax(SFunction<T, ?> column) {
return selectMax(column, AggregateSelectUtils.aliasName(column));
}
/**
* 指定 MAX 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名
* @return this
*/
public LambdaCrudChainWrapper<T, V> selectMax(SFunction<T, ?> column, String alias) {
return selectAggregate(SqlAggregateFunction.MAX, column, alias);
}
/**
* 指定 MAX 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名字段
* @param <A> 查询结果类型
* @return this
*/
public <A> LambdaCrudChainWrapper<T, V> selectMax(SFunction<T, ?> column, SFunction<A, ?> alias) {
return selectMax(column, AggregateSelectUtils.aliasName(alias));
}
/**
* 指定 MIN 聚合查询字段。
*
* @param column 聚合字段
* @return this
*/
public LambdaCrudChainWrapper<T, V> selectMin(SFunction<T, ?> column) {
return selectMin(column, AggregateSelectUtils.aliasName(column));
}
/**
* 指定 MIN 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名
* @return this
*/
public LambdaCrudChainWrapper<T, V> selectMin(SFunction<T, ?> column, String alias) {
return selectAggregate(SqlAggregateFunction.MIN, column, alias);
}
/**
* 指定 MIN 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名字段
* @param <A> 查询结果类型
* @return this
*/
public <A> LambdaCrudChainWrapper<T, V> selectMin(SFunction<T, ?> column, SFunction<A, ?> alias) {
return selectMin(column, AggregateSelectUtils.aliasName(alias));
}
/**
* 指定 AVG 聚合查询字段。
*
* @param column 聚合字段
* @return this
*/
public LambdaCrudChainWrapper<T, V> selectAvg(SFunction<T, ?> column) {
return selectAvg(column, AggregateSelectUtils.aliasName(column));
}
/**
* 指定 AVG 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名
* @return this
*/
public LambdaCrudChainWrapper<T, V> selectAvg(SFunction<T, ?> column, String alias) {
return selectAggregate(SqlAggregateFunction.AVG, column, alias);
}
/**
* 指定 AVG 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名字段
* @param <A> 查询结果类型
* @return this
*/
public <A> LambdaCrudChainWrapper<T, V> selectAvg(SFunction<T, ?> column, SFunction<A, ?> alias) {
return selectAvg(column, AggregateSelectUtils.aliasName(alias));
}
/**
* 指定 COUNT 聚合查询字段。
*
* @param column 聚合字段
* @return this
*/
public LambdaCrudChainWrapper<T, V> selectCount(SFunction<T, ?> column) {
return selectCount(column, AggregateSelectUtils.aliasName(column));
}
/**
* 指定 COUNT 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名
* @return this
*/
public LambdaCrudChainWrapper<T, V> selectCount(SFunction<T, ?> column, String alias) {
return selectAggregate(SqlAggregateFunction.COUNT, column, alias);
}
/**
* 指定 COUNT 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名字段
* @param <A> 查询结果类型
* @return this
*/
public <A> LambdaCrudChainWrapper<T, V> selectCount(SFunction<T, ?> column, SFunction<A, ?> alias) {
return selectCount(column, AggregateSelectUtils.aliasName(alias));
}
/**
* 指定 COUNT(*) 聚合查询字段。
*
* @param alias 查询别名
* @return this
*/
public LambdaCrudChainWrapper<T, V> selectCountAll(String alias) {
return selectAggregate(SqlAggregateFunction.COUNT, "*", alias);
}
/**
* 指定 COUNT(*) 聚合查询字段。
*
* @param alias 查询别名字段
* @param <A> 查询结果类型
* @return this
*/
public <A> LambdaCrudChainWrapper<T, V> selectCountAll(SFunction<A, ?> alias) {
return selectCountAll(AggregateSelectUtils.aliasName(alias));
}
/**
* 指定 COUNT(DISTINCT column) 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名
* @return this
*/
public LambdaCrudChainWrapper<T, V> selectCountDistinct(SFunction<T, ?> column, String alias) {
return selectAggregate(SqlAggregateFunction.COUNT, "DISTINCT " + columnToString(column), alias);
}
/**
* 指定 COUNT(DISTINCT column) 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名字段
* @param <A> 查询结果类型
* @return this
*/
public <A> LambdaCrudChainWrapper<T, V> selectCountDistinct(SFunction<T, ?> column, SFunction<A, ?> alias) {
return selectCountDistinct(column, AggregateSelectUtils.aliasName(alias));
}
/**
* 指定子查询字段。
* <pre>{@code
* userMapper.lambda()
* .select(SysUser::getUserId, SysUser::getUserName)
* .selectSub(SysUserRole.class, sub -> sub
* .selectCountAll()
* .eqColumn(SysUserRole::getUserId, SysUser::getUserId),
* UserStatVo::getRoleCount)
* .voList();
* }</pre>
*
* @param entityClass 子查询实体类型
* @param consumer 子查询构造函数
* @param alias 查询别名
* @param <S> 子查询实体类型
* @return this
*/
public <S> LambdaCrudChainWrapper<T, V> selectSub(Class<S> entityClass, Consumer<SubQuery<S>> consumer, String alias) {
sqlSelect.setStringValue(AggregateSelectUtils.appendSelect(sqlSelect.getStringValue(),
AggregateSelectUtils.subquerySelect(buildSubQuery(entityClass, consumer), alias)));
return typedThis;
}
/**
* 指定子查询字段。
*
* @param entityClass 子查询实体类型
* @param consumer 子查询构造函数
* @param alias 查询别名字段
* @param <S> 子查询实体类型
* @param <A> 查询结果类型
* @return this
*/
public <S, A> LambdaCrudChainWrapper<T, V> selectSub(Class<S> entityClass, Consumer<SubQuery<S>> consumer, SFunction<A, ?> alias) {
return selectSub(entityClass, consumer, AggregateSelectUtils.aliasName(alias));
}
/**
* 添加等于子查询条件。
* <pre>{@code
* userMapper.lambda()
* .eqSub(SysUser::getDeptId, SysDept.class, sub -> sub
* .select(SysDept::getDeptId)
* .eq(SysDept::getDeptName, deptName))
* .voList();
* }</pre>
*
* @param column 字段
* @param entityClass 子查询实体类型
* @param consumer 子查询构造函数
* @param <S> 子查询实体类型
* @return this
*/
public <S> LambdaCrudChainWrapper<T, V> eqSub(SFunction<T, ?> column, Class<S> entityClass, Consumer<SubQuery<S>> consumer) {
return super.eqSql(true, column, buildSubQuery(entityClass, consumer));
}
/**
* 添加 IN 子查询条件。
* <pre>{@code
* userMapper.lambda()
* .inSub(SysUser::getUserId, SysUserRole.class, sub -> sub
* .select(SysUserRole::getUserId)
* .eq(SysUserRole::getRoleId, roleId))
* .voList();
* }</pre>
*
* @param column 字段
* @param entityClass 子查询实体类型
* @param consumer 子查询构造函数
* @param <S> 子查询实体类型
* @return this
*/
public <S> LambdaCrudChainWrapper<T, V> inSub(SFunction<T, ?> column, Class<S> entityClass, Consumer<SubQuery<S>> consumer) {
return super.inSql(true, column, buildSubQuery(entityClass, consumer));
}
/**
* 添加 NOT IN 子查询条件。
*
* @param column 字段
* @param entityClass 子查询实体类型
* @param consumer 子查询构造函数
* @param <S> 子查询实体类型
* @return this
*/
public <S> LambdaCrudChainWrapper<T, V> notInSub(SFunction<T, ?> column, Class<S> entityClass, Consumer<SubQuery<S>> consumer) {
return super.notInSql(true, column, buildSubQuery(entityClass, consumer));
}
/**
* 添加 EXISTS 子查询条件。
* <pre>{@code
* userMapper.lambda()
* .existsSub(SysUserRole.class, sub -> sub
* .selectCountAll()
* .eqColumn(SysUserRole::getUserId, SysUser::getUserId)
* .eq(SysUserRole::getRoleId, roleId))
* .voList();
* }</pre>
*
* @param entityClass 子查询实体类型
* @param consumer 子查询构造函数
* @param <S> 子查询实体类型
* @return this
*/
public <S> LambdaCrudChainWrapper<T, V> existsSub(Class<S> entityClass, Consumer<SubQuery<S>> consumer) {
return super.exists(true, buildSubQuery(entityClass, consumer));
}
/**
* 添加 NOT EXISTS 子查询条件。
*
* @param entityClass 子查询实体类型
* @param consumer 子查询构造函数
* @param <S> 子查询实体类型
* @return this
*/
public <S> LambdaCrudChainWrapper<T, V> notExistsSub(Class<S> entityClass, Consumer<SubQuery<S>> consumer) {
return super.notExists(true, buildSubQuery(entityClass, consumer));
}
/**
* 按字段过滤条件选择查询字段。
*
* @param entityClass 实体类型
* @param predicate 字段过滤条件
* @return this
*/
@Override
public LambdaCrudChainWrapper<T, V> select(Class<T> entityClass, Predicate<TableFieldInfo> predicate) {
if (entityClass == null) {
entityClass = getEntityClass();
} else {
setEntityClass(entityClass);
}
Assert.notNull(entityClass, "entityClass can not be null");
this.sqlSelect.setStringValue(TableInfoHelper.getTableInfo(entityClass).chooseSelect(predicate));
return typedThis;
}
/**
* 获取查询字段 SQL 片段。
*
* @return 查询字段 SQL 片段
*/
@Override
public String getSqlSelect() {
return sqlSelect.getStringValue();
}
/**
* 指定聚合查询字段。
*
* @param function 聚合函数
* @param column 聚合字段
* @param alias 查询别名
* @return this
*/
private LambdaCrudChainWrapper<T, V> selectAggregate(SqlAggregateFunction function, SFunction<T, ?> column, String alias) {
return selectAggregate(function, columnToString(column), alias);
}
/**
* 指定聚合查询字段。
*
* @param function 聚合函数
* @param column 聚合字段 SQL
* @param alias 查询别名
* @return this
*/
private LambdaCrudChainWrapper<T, V> selectAggregate(SqlAggregateFunction function, String column, String alias) {
sqlSelect.setStringValue(AggregateSelectUtils.appendSelect(sqlSelect.getStringValue(),
AggregateSelectUtils.aggregateSelect(function, column, alias)));
return typedThis;
}
/**
* 构建子查询 SQL。
*
* @param entityClass 子查询实体类型
* @param consumer 子查询构造函数
* @param <S> 子查询实体类型
* @return 子查询 SQL
*/
private <S> String buildSubQuery(Class<S> entityClass, Consumer<SubQuery<S>> consumer) {
SubQuery<S> subQuery = SubQuery.of(entityClass, value -> formatParam(null, value));
consumer.accept(subQuery);
return subQuery.build();
}
/**
* 按条件设置更新字段。
*
* @param condition 是否设置该字段
* @param column 字段
* @param val 字段值
* @param mapping 参数映射
* @return this
*/
@Override
public LambdaCrudChainWrapper<T, V> set(boolean condition, SFunction<T, ?> column, Object val, String mapping) {
return maybeDo(condition, () -> {
String sql = formatParam(mapping, val);
sqlSet.add(columnToString(column) + Constants.EQUALS + sql);
});
}
/**
* 值不为 null 时设置更新字段。
*
* @param column 字段
* @param value 值
* @return this
*/
public LambdaCrudChainWrapper<T, V> setIfPresent(SFunction<T, ?> column, Object value) {
return set(value != null, column, value);
}
/**
* 文本不为空时设置更新字段。
*
* @param column 字段
* @param value 值
* @return this
*/
public LambdaCrudChainWrapper<T, V> setIfText(SFunction<T, ?> column, String value) {
return set(StringUtils.isNotBlank(value), column, value);
}
/**
* 按条件设置自定义 SQL 更新片段。
*
* @param condition 是否设置该片段
* @param setSql SQL 更新片段
* @param params SQL 片段参数
* @return this
*/
@Override
public LambdaCrudChainWrapper<T, V> setSql(boolean condition, String setSql, Object... params) {
return maybeDo(condition && StringUtils.isNotBlank(setSql), () -> sqlSet.add(formatSqlMaybeWithParam(setSql, params)));
}
/**
* 按条件设置字段自增。
*
* @param condition 是否设置该字段
* @param column 字段
* @param val 自增值
* @return this
*/
@Override
public LambdaCrudChainWrapper<T, V> setIncrBy(boolean condition, SFunction<T, ?> column, Number val) {
return maybeDo(condition, () -> {
String realColumn = columnToString(column);
String realVal = val instanceof BigDecimal ? ((BigDecimal) val).toPlainString() : String.valueOf(val);
sqlSet.add(String.format("%s=%s + %s", realColumn, realColumn, realVal));
});
}
/**
* 按条件设置字段自减。
*
* @param condition 是否设置该字段
* @param column 字段
* @param val 自减值
* @return this
*/
@Override
public LambdaCrudChainWrapper<T, V> setDecrBy(boolean condition, SFunction<T, ?> column, Number val) {
return maybeDo(condition, () -> {
String realColumn = columnToString(column);
String realVal = val instanceof BigDecimal ? ((BigDecimal) val).toPlainString() : String.valueOf(val);
sqlSet.add(String.format("%s=%s - %s", realColumn, realColumn, realVal));
});
}
/**
* 获取更新 set SQL 片段。
*
* @return 更新 set SQL 片段
*/
@Override
public String getSqlSet() {
if (CollectionUtils.isEmpty(sqlSet)) {
return null;
}
return String.join(Constants.COMMA, sqlSet);
}
/**
* 获取查询条件 Wrapper。
*
* @return this
*/
public LambdaCrudChainWrapper<T, V> getWrapper() {
return typedThis;
}
/**
* 添加 FIND_IN_SET 条件。
*
* @param value 匹配值
* @param column 字段
* @return this
*/
public LambdaCrudChainWrapper<T, V> findInSet(Object value, SFunction<T, ?> column) {
return findInSet(true, value, column);
}
/**
* 添加 FIND_IN_SET 条件。
*
* @param condition 是否添加该条件
* @param value 匹配值
* @param column 字段
* @return this
*/
public LambdaCrudChainWrapper<T, V> findInSet(boolean condition, Object value, SFunction<T, ?> column) {
return findInSet(condition, value, columnToString(column));
}
/**
* 值不为空时添加 FIND_IN_SET 条件。
*
* @param value 匹配值
* @param column 字段
* @return this
*/
public LambdaCrudChainWrapper<T, V> findInSetIfPresent(Object value, SFunction<T, ?> column) {
return findInSet(value != null, value, column);
}
/**
* 获取当前 Wrapper。
*
* @return this
*/
public LambdaCrudChainWrapper<T, V> build() {
return typedThis;
}
/**
* 查询实体列表。
*
* @return 实体列表
*/
public List<T> list() {
return crudMapper.selectList(typedThis);
}
/**
* 查询实体分页记录。
*
* @param page 分页条件
* @return 实体分页记录
*/
public List<T> list(IPage<T> page) {
return crudMapper.selectList(page, typedThis);
}
/**
* 查询 VO 列表。
*
* @return VO 列表
*/
public List<V> voList() {
return crudMapper.selectVoList(typedThis);
}
/**
* 查询单列对象列表。
*
* @return 单列对象列表
*/
public List<Object> objs() {
return crudMapper.selectObjs(typedThis);
}
/**
* 查询单列对象列表并转换类型。
*
* @param mapper 转换函数
* @param <C> 转换后的类型
* @return 单列对象列表
*/
public <C> List<C> objs(Function<? super Object, C> mapper) {
return crudMapper.selectObjs(typedThis, mapper);
}
/**
* 查询单个实体。
*
* @return 实体
*/
public T one() {
return crudMapper.selectOne(typedThis);
}
/**
* 查询单个实体。
*
* @param throwEx 查询到多条时是否抛异常
* @return 实体
*/
public T one(boolean throwEx) {
return crudMapper.selectOne(typedThis, throwEx);
}
/**
* 查询单个实体 Optional。
*
* @return Optional 实体
*/
public Optional<T> oneOpt() {
return Optional.ofNullable(one());
}
/**
* 查询单个 VO。
*
* @return VO
*/
public V voOne() {
return crudMapper.selectVoOne(typedThis);
}
/**
* 查询单个 VO。
*
* @param throwEx 查询到多条时是否抛异常
* @return VO
*/
public V voOne(boolean throwEx) {
return crudMapper.selectVoOne(typedThis, throwEx);
}
/**
* 查询数量。
*
* @return 数量
*/
public Long count() {
return crudMapper.selectCount(typedThis);
}
/**
* 判断是否存在。
*
* @return 是否存在
*/
public boolean exists() {
return crudMapper.exists(typedThis);
}
/**
* 查询实体分页。
*
* @param page 分页条件
* @param <P> 分页类型
* @return 实体分页
*/
public <P extends IPage<T>> P page(P page) {
return crudMapper.selectPage(page, typedThis);
}
/**
* 查询 VO 分页。
*
* @param page 分页条件
* @param <P> 分页类型
* @return VO 分页
*/
public <P extends IPage<V>> P voPage(IPage<T> page) {
return crudMapper.selectVoPage(page, typedThis);
}
/**
* 删除数据。
*
* @return 是否删除成功
*/
public boolean delete() {
return deleteCount() > 0;
}
/**
* 删除数据。
*
* @return 影响行数
*/
public int deleteCount() {
return crudMapper.delete(typedThis);
}
/**
* 使用 set 片段更新数据。
*
* @return 是否更新成功
*/
public boolean update() {
return updateCount() > 0;
}
/**
* 使用实体和查询条件更新数据。
*
* @param entity 实体
* @return 是否更新成功
*/
public boolean update(T entity) {
return updateCount(entity) > 0;
}
/**
* 使用 set 片段更新数据。
*
* @return 影响行数
*/
public int updateCount() {
return crudMapper.update(typedThis);
}
/**
* 使用实体和查询条件更新数据。
*
* @param entity 实体
* @return 影响行数
*/
public int updateCount(T entity) {
return crudMapper.update(entity, typedThis);
}
/**
* 创建新的链式包装器实例。
*
* @return 新的链式包装器实例
*/
@Override
protected LambdaCrudChainWrapper<T, V> instance() {
return new LambdaCrudChainWrapper<>(crudMapper, getEntity(), getEntityClass(), null, null, paramNameSeq,
paramNameValuePairs, new MergeSegments(), paramAlias, SharedString.emptyString(), SharedString.emptyString(),
SharedString.emptyString());
}
/**
* 清空当前 Wrapper 状态。
*/
@Override
public void clear() {
super.clear();
sqlSelect.toNull();
sqlSet.clear();
}
}
@@ -0,0 +1,148 @@
package org.dromara.common.mybatis.core.page;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.sql.SqlUtil;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 分页查询实体类
*
* @author Lion Li
*/
@Data
@NoArgsConstructor
public class PageQuery implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 分页大小
*/
private Integer pageSize;
/**
* 当前页数
*/
private Integer pageNum;
/**
* 排序列
*/
private String orderByColumn;
/**
* 排序的方向desc或者asc
*/
private String isAsc;
/**
* 当前记录起始索引 默认值
*/
public static final int DEFAULT_PAGE_NUM = 1;
/**
* 每页显示记录数 默认值 默认查全部
*/
public static final int DEFAULT_PAGE_SIZE = Integer.MAX_VALUE;
/**
* 构建分页对象。
*
* @param <T> 分页记录类型
* @return MyBatis-Plus 分页对象
*/
public <T> Page<T> build() {
Integer pageNum = ObjectUtil.defaultIfNull(getPageNum(), DEFAULT_PAGE_NUM);
Integer pageSize = ObjectUtil.defaultIfNull(getPageSize(), DEFAULT_PAGE_SIZE);
if (pageNum <= 0) {
pageNum = DEFAULT_PAGE_NUM;
}
Page<T> page = new Page<>(pageNum, pageSize);
List<OrderItem> orderItems = buildOrderItem();
if (CollUtil.isNotEmpty(orderItems)) {
page.addOrder(orderItems);
}
return page;
}
/**
* 构建排序
* <p>
* 支持的用法如下:
* {isAsc:"asc",orderByColumn:"id"} order by id asc
* {isAsc:"asc",orderByColumn:"id,createTime"} order by id asc,create_time asc
* {isAsc:"desc",orderByColumn:"id,createTime"} order by id desc,create_time desc
* {isAsc:"asc,desc",orderByColumn:"id,createTime"} order by id asc,create_time desc
*/
private List<OrderItem> buildOrderItem() {
if (StringUtils.isBlank(orderByColumn) || StringUtils.isBlank(isAsc)) {
return List.of();
}
String orderBy = SqlUtil.escapeOrderBySql(orderByColumn);
orderBy = StringUtils.toUnderScoreCase(orderBy);
// 兼容前端排序类型
String orderDirection = StringUtils.replaceEach(isAsc, new String[]{"ascending", "descending"}, new String[]{"asc", "desc"});
String[] orderByArr = orderBy.split(StringUtils.SEPARATOR);
String[] isAscArr = orderDirection.split(StringUtils.SEPARATOR);
if (isAscArr.length != 1 && isAscArr.length != orderByArr.length) {
throw new ServiceException("排序参数有误");
}
List<OrderItem> list = new ArrayList<>();
// 每个字段各自排序
for (int i = 0; i < orderByArr.length; i++) {
String orderByStr = orderByArr[i];
String isAscStr = isAscArr.length == 1 ? isAscArr[0] : isAscArr[i];
if ("asc".equals(isAscStr)) {
list.add(OrderItem.asc(orderByStr));
} else if ("desc".equals(isAscStr)) {
list.add(OrderItem.desc(orderByStr));
} else {
throw new ServiceException("排序参数有误");
}
}
return list;
}
/**
* 获取当前页起始行号。
*
* @return 起始行号
*/
@JsonIgnore
public Integer getFirstNum() {
Integer currentPageNum = ObjectUtil.defaultIfNull(getPageNum(), DEFAULT_PAGE_NUM);
Integer currentPageSize = ObjectUtil.defaultIfNull(getPageSize(), DEFAULT_PAGE_SIZE);
if (currentPageNum <= 0) {
currentPageNum = DEFAULT_PAGE_NUM;
}
return (currentPageNum - 1) * currentPageSize;
}
/**
* 构造分页查询对象。
*
* @param pageSize 分页大小
* @param pageNum 当前页码
*/
public PageQuery(Integer pageSize, Integer pageNum) {
this.pageSize = pageSize;
this.pageNum = pageNum;
}
}
@@ -0,0 +1,108 @@
package org.dromara.common.mybatis.core.query;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
/**
* 支持追加聚合查询字段的 Lambda 查询包装器。
*
* @param <T> 实体类型
* @author Lion Li
*/
class AggregateLambdaQueryWrapper<T> extends LambdaQueryWrapper<T> {
/**
* 追加后的聚合查询字段 SQL。
*/
private String aggregateSqlSelect;
/**
* 构造聚合查询包装器。
*
* @param entityClass 实体类型
*/
AggregateLambdaQueryWrapper(Class<T> entityClass) {
super(entityClass);
}
/**
* 追加聚合查询字段。
*
* @param columnSql 查询字段 SQL
*/
void appendSelectSql(String columnSql) {
aggregateSqlSelect = AggregateSelectUtils.appendSelect(getSqlSelect(), columnSql);
}
/**
* 追加子查询字段。
*
* @param subquerySql 子查询 SQL
* @param alias 查询别名
* @param params 子查询参数
*/
void appendSelectSub(String subquerySql, String alias, Object... params) {
appendSelectSql(AggregateSelectUtils.subquerySelect(formatSqlMaybeWithParam(subquerySql, params), alias));
}
/**
* 格式化子查询 SQL。
*
* @param subquerySql 子查询 SQL
* @param params 子查询参数
* @return 格式化后的子查询 SQL
*/
String formatSubquerySql(String subquerySql, Object... params) {
return formatSqlMaybeWithParam(subquerySql, params);
}
/**
* 格式化子查询参数。
*
* @param value 参数值
* @return MyBatis 参数占位符
*/
String formatSubqueryParam(Object value) {
return formatParam(null, value);
}
/**
* 清空聚合查询字段。
*/
void resetAggregateSelect() {
aggregateSqlSelect = null;
}
/**
* 获取字段对应的数据库列名。
*
* @param column 字段
* @return 数据库列名
*/
String columnName(SFunction<T, ?> column) {
return columnToString(column);
}
/**
* 获取最终查询字段 SQL。
*
* @return 查询字段 SQL
*/
@Override
public String getSqlSelect() {
if (aggregateSqlSelect != null) {
return aggregateSqlSelect;
}
return super.getSqlSelect();
}
/**
* 清空查询条件与聚合查询字段。
*/
@Override
public void clear() {
super.clear();
aggregateSqlSelect = null;
}
}
@@ -0,0 +1,87 @@
package org.dromara.common.mybatis.core.query;
import com.baomidou.mybatisplus.core.toolkit.Assert;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.support.LambdaMeta;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.apache.ibatis.reflection.property.PropertyNamer;
import java.util.regex.Pattern;
/**
* 聚合查询字段 SQL 构造工具。
*
* @author Lion Li
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class AggregateSelectUtils {
/**
* 查询别名合法性匹配规则。
*/
private static final Pattern ALIAS_PATTERN = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*");
/**
* 追加查询字段 SQL。
*
* @param current 已有查询字段 SQL
* @param fragment 新增查询字段 SQL
* @return 合并后的查询字段 SQL
*/
public static String appendSelect(String current, String fragment) {
if (StringUtils.isBlank(current)) {
return fragment;
}
return current + Constants.COMMA + fragment;
}
/**
* 生成聚合查询字段 SQL。
*
* @param function 聚合函数
* @param column 字段 SQL
* @param alias 查询别名
* @return 聚合查询字段 SQL
*/
public static String aggregateSelect(SqlAggregateFunction function, String column, String alias) {
return function.format(column) + " AS " + checkAlias(alias);
}
/**
* 生成子查询字段 SQL。
*
* @param subquerySql 子查询 SQL
* @param alias 查询别名
* @return 子查询字段 SQL
*/
public static String subquerySelect(String subquerySql, String alias) {
return "(" + subquerySql + ") AS " + checkAlias(alias);
}
/**
* 从 Lambda Getter 解析属性名作为查询别名。
*
* @param alias 别名字段 Getter
* @return 查询别名
*/
public static String aliasName(SFunction<?, ?> alias) {
LambdaMeta meta = com.baomidou.mybatisplus.core.toolkit.LambdaUtils.extract(alias);
return PropertyNamer.methodToProperty(meta.getImplMethodName());
}
/**
* 检查查询别名是否为通用 SQL 标识符。
*
* @param alias 查询别名
* @return 查询别名
*/
public static String checkAlias(String alias) {
Assert.isTrue(StringUtils.isNotBlank(alias) && ALIAS_PATTERN.matcher(alias).matches(),
"查询别名只能包含字母、数字、下划线且不能以数字开头: %s", alias);
return alias;
}
}
@@ -0,0 +1,946 @@
package org.dromara.common.mybatis.core.query;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.github.yulichang.toolkit.LambdaUtils;
import com.github.yulichang.toolkit.support.ColumnCache;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import com.github.yulichang.wrapper.enums.DefaultFuncEnum;
import com.github.yulichang.wrapper.segments.SelectCache;
import com.github.yulichang.wrapper.segments.SelectNormal;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.helper.DataBaseHelper;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
/**
* MPJ Lambda 联表查询构造辅助类。
*
* @param <T> 主表实体类型
* @author Lion Li
*/
public final class LambdaJoinQueryBuilder<T> {
/**
* MyBatis-Plus-Join Lambda 查询包装器。
*/
private final MPJLambdaWrapper<T> wrapper;
/**
* 构造 MPJ Lambda 联表查询构造辅助对象。
*
* @param wrapper MPJ Lambda 查询包装器
*/
LambdaJoinQueryBuilder(MPJLambdaWrapper<T> wrapper) {
this.wrapper = wrapper;
}
/**
* 添加去重查询。
*
* @return 当前联表查询构造辅助对象
*/
public LambdaJoinQueryBuilder<T> distinct() {
wrapper.distinct();
return this;
}
/**
* 指定主表查询字段。
*
* @param columns 查询字段
* @return 当前联表查询构造辅助对象
*/
@SafeVarargs
public final <E> LambdaJoinQueryBuilder<T> select(SFunction<E, ?>... columns) {
wrapper.select(columns);
return this;
}
/**
* 指定带表别名的同名映射查询字段。
*
* @param alias 表别名
* @param columns 查询字段
* @return 当前联表查询构造辅助对象
*/
@SafeVarargs
public final <E> LambdaJoinQueryBuilder<T> select(String alias, SFunction<E, ?>... columns) {
if (columns == null || columns.length == 0) {
return this;
}
Class<?> entityClass = LambdaUtils.getEntityClass(columns[0]);
Map<String, SelectCache> cacheMap = ColumnCache.getMapField(entityClass);
for (SFunction<E, ?> column : columns) {
wrapper.getSelectColum().add(new SelectNormal(cacheMap.get(LambdaUtils.getName(column)), wrapper.getIndex(), true, alias));
}
return this;
}
/**
* 查询主表全部字段。
*
* @return 当前联表查询构造辅助对象
*/
public LambdaJoinQueryBuilder<T> selectAll() {
wrapper.selectAll();
return this;
}
/**
* 查询指定实体全部字段。
*
* @param entityClass 实体类型
* @return 当前联表查询构造辅助对象
*/
public LambdaJoinQueryBuilder<T> selectAll(Class<?> entityClass) {
wrapper.selectAll(entityClass);
return this;
}
/**
* 查询指定别名实体全部字段。
*
* @param entityClass 实体类型
* @param alias 表别名
* @return 当前联表查询构造辅助对象
*/
public LambdaJoinQueryBuilder<T> selectAll(Class<?> entityClass, String alias) {
wrapper.selectAll(entityClass, alias);
return this;
}
/**
* 指定查询字段并映射到返回对象字段。
*
* @param column 查询字段
* @param alias 返回对象字段
* @return 当前联表查询构造辅助对象
*/
public <S, X> LambdaJoinQueryBuilder<T> selectAs(SFunction<S, ?> column, SFunction<X, ?> alias) {
wrapper.selectAs(column, alias);
return this;
}
/**
* 指定带表别名的查询字段并映射到返回对象字段。
*
* @param tableAlias 表别名
* @param column 查询字段
* @param alias 返回对象字段
* @return 当前联表查询构造辅助对象
*/
public <S, X> LambdaJoinQueryBuilder<T> selectAs(String tableAlias, SFunction<S, ?> column, SFunction<X, ?> alias) {
wrapper.selectAs(tableAlias, column, alias);
return this;
}
/**
* 指定 SQL 查询片段并映射到返回对象字段。
*
* @param column SQL 查询片段
* @param alias 返回对象字段
* @return 当前联表查询构造辅助对象
*/
public <X> LambdaJoinQueryBuilder<T> selectAs(String column, SFunction<X, ?> alias) {
wrapper.selectAs(column, alias);
return this;
}
/**
* 指定子查询字段。
* <pre>{@code
* QueryBuilder.lambdaJoin("u", SysUser.class)
* .selectAs("u", SysUser::getUserId, UserStatVo::getUserId)
* .selectSub(SysUserRole.class, sub -> sub
* .selectCountAll()
* .eqColumn(SysUserRole::getUserId, "u", SysUser::getUserId),
* UserStatVo::getRoleCount)
* .list(UserStatVo.class);
* }</pre>
*
* @param entityClass 子查询实体类型
* @param consumer 子查询构造函数
* @param alias 查询别名
* @param <S> 子查询实体类型
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> selectSub(Class<S> entityClass, Consumer<SubQuery<S>> consumer, String alias) {
SubQuery<S> subQuery = buildPlaceholderSubQuery(entityClass, consumer);
wrapper.selectFunc("(" + subQuery.build() + ")", func -> func.values(subQuery.params()),
AggregateSelectUtils.checkAlias(alias));
return this;
}
/**
* 指定子查询字段。
*
* @param entityClass 子查询实体类型
* @param consumer 子查询构造函数
* @param alias 查询别名字段
* @param <S> 子查询实体类型
* @param <X> 查询结果类型
* @return 当前联表查询构造辅助对象
*/
public <S, X> LambdaJoinQueryBuilder<T> selectSub(Class<S> entityClass, Consumer<SubQuery<S>> consumer, SFunction<X, ?> alias) {
return selectSub(entityClass, consumer, AggregateSelectUtils.aliasName(alias));
}
/**
* 指定 SUM 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> selectSum(SFunction<S, ?> column, String alias) {
wrapper.selectSum(column, AggregateSelectUtils.checkAlias(alias));
return this;
}
/**
* 指定 SUM 聚合查询字段。
*
* @param tableAlias 表别名
* @param column 聚合字段
* @param alias 查询别名
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> selectSum(String tableAlias, SFunction<S, ?> column, String alias) {
wrapper.selectFunc(DefaultFuncEnum.SUM, tableAlias, column, AggregateSelectUtils.checkAlias(alias));
return this;
}
/**
* 指定 SUM 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名字段
* @return 当前联表查询构造辅助对象
*/
public <S, X> LambdaJoinQueryBuilder<T> selectSum(SFunction<S, ?> column, SFunction<X, ?> alias) {
return selectSum(column, AggregateSelectUtils.aliasName(alias));
}
/**
* 指定 MAX 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> selectMax(SFunction<S, ?> column, String alias) {
wrapper.selectMax(column, AggregateSelectUtils.checkAlias(alias));
return this;
}
/**
* 指定 MAX 聚合查询字段。
*
* @param tableAlias 表别名
* @param column 聚合字段
* @param alias 查询别名
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> selectMax(String tableAlias, SFunction<S, ?> column, String alias) {
wrapper.selectFunc(DefaultFuncEnum.MAX, tableAlias, column, AggregateSelectUtils.checkAlias(alias));
return this;
}
/**
* 指定 MAX 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名字段
* @return 当前联表查询构造辅助对象
*/
public <S, X> LambdaJoinQueryBuilder<T> selectMax(SFunction<S, ?> column, SFunction<X, ?> alias) {
return selectMax(column, AggregateSelectUtils.aliasName(alias));
}
/**
* 指定 MIN 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> selectMin(SFunction<S, ?> column, String alias) {
wrapper.selectMin(column, AggregateSelectUtils.checkAlias(alias));
return this;
}
/**
* 指定 MIN 聚合查询字段。
*
* @param tableAlias 表别名
* @param column 聚合字段
* @param alias 查询别名
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> selectMin(String tableAlias, SFunction<S, ?> column, String alias) {
wrapper.selectFunc(DefaultFuncEnum.MIN, tableAlias, column, AggregateSelectUtils.checkAlias(alias));
return this;
}
/**
* 指定 MIN 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名字段
* @return 当前联表查询构造辅助对象
*/
public <S, X> LambdaJoinQueryBuilder<T> selectMin(SFunction<S, ?> column, SFunction<X, ?> alias) {
return selectMin(column, AggregateSelectUtils.aliasName(alias));
}
/**
* 指定 AVG 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> selectAvg(SFunction<S, ?> column, String alias) {
wrapper.selectAvg(column, AggregateSelectUtils.checkAlias(alias));
return this;
}
/**
* 指定 AVG 聚合查询字段。
*
* @param tableAlias 表别名
* @param column 聚合字段
* @param alias 查询别名
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> selectAvg(String tableAlias, SFunction<S, ?> column, String alias) {
wrapper.selectFunc(DefaultFuncEnum.AVG, tableAlias, column, AggregateSelectUtils.checkAlias(alias));
return this;
}
/**
* 指定 AVG 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名字段
* @return 当前联表查询构造辅助对象
*/
public <S, X> LambdaJoinQueryBuilder<T> selectAvg(SFunction<S, ?> column, SFunction<X, ?> alias) {
return selectAvg(column, AggregateSelectUtils.aliasName(alias));
}
/**
* 指定 COUNT 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> selectCount(SFunction<S, ?> column, String alias) {
wrapper.selectCount(column, AggregateSelectUtils.checkAlias(alias));
return this;
}
/**
* 指定 COUNT 聚合查询字段。
*
* @param tableAlias 表别名
* @param column 聚合字段
* @param alias 查询别名
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> selectCount(String tableAlias, SFunction<S, ?> column, String alias) {
wrapper.selectFunc(DefaultFuncEnum.COUNT, tableAlias, column, AggregateSelectUtils.checkAlias(alias));
return this;
}
/**
* 指定 COUNT 聚合查询字段。
*
* @param column 聚合字段
* @param alias 查询别名字段
* @return 当前联表查询构造辅助对象
*/
public <S, X> LambdaJoinQueryBuilder<T> selectCount(SFunction<S, ?> column, SFunction<X, ?> alias) {
return selectCount(column, AggregateSelectUtils.aliasName(alias));
}
/**
* 指定 COUNT(*) 聚合查询字段。
*
* @param alias 查询别名
* @return 当前联表查询构造辅助对象
*/
public LambdaJoinQueryBuilder<T> selectCountAll(String alias) {
wrapper.select(AggregateSelectUtils.aggregateSelect(SqlAggregateFunction.COUNT, "*", alias));
return this;
}
/**
* 指定 COUNT(*) 聚合查询字段。
*
* @param alias 查询别名字段
* @return 当前联表查询构造辅助对象
*/
public <X> LambdaJoinQueryBuilder<T> selectCountAll(SFunction<X, ?> alias) {
return selectCountAll(AggregateSelectUtils.aliasName(alias));
}
/**
* 添加等于子查询条件。
*
* @param alias 表别名
* @param column 字段
* @param entityClass 子查询实体类型
* @param consumer 子查询构造函数
* @param <S> 字段实体类型
* @param <Q> 子查询实体类型
* @return 当前联表查询构造辅助对象
*/
public <S, Q> LambdaJoinQueryBuilder<T> eqSub(String alias, SFunction<S, ?> column, Class<Q> entityClass,
Consumer<SubQuery<Q>> consumer) {
SubQuery<Q> subQuery = buildPlaceholderSubQuery(entityClass, consumer);
wrapper.apply(true, qualifiedColumn(alias, column) + " = (" + subQuery.build() + ")", subQuery.params());
return this;
}
/**
* 添加 IN 子查询条件。
* <pre>{@code
* QueryBuilder.lambdaJoin("u", SysUser.class)
* .inSub("u", SysUser::getUserId, SysUserRole.class, sub -> sub
* .select(SysUserRole::getUserId)
* .eq(SysUserRole::getRoleId, roleId))
* .list(SysUserVo.class);
* }</pre>
*
* @param alias 表别名
* @param column 字段
* @param entityClass 子查询实体类型
* @param consumer 子查询构造函数
* @param <S> 字段实体类型
* @param <Q> 子查询实体类型
* @return 当前联表查询构造辅助对象
*/
public <S, Q> LambdaJoinQueryBuilder<T> inSub(String alias, SFunction<S, ?> column, Class<Q> entityClass,
Consumer<SubQuery<Q>> consumer) {
SubQuery<Q> subQuery = buildPlaceholderSubQuery(entityClass, consumer);
wrapper.apply(true, qualifiedColumn(alias, column) + " IN (" + subQuery.build() + ")", subQuery.params());
return this;
}
/**
* 添加 NOT IN 子查询条件。
*
* @param alias 表别名
* @param column 字段
* @param entityClass 子查询实体类型
* @param consumer 子查询构造函数
* @param <S> 字段实体类型
* @param <Q> 子查询实体类型
* @return 当前联表查询构造辅助对象
*/
public <S, Q> LambdaJoinQueryBuilder<T> notInSub(String alias, SFunction<S, ?> column, Class<Q> entityClass,
Consumer<SubQuery<Q>> consumer) {
SubQuery<Q> subQuery = buildPlaceholderSubQuery(entityClass, consumer);
wrapper.apply(true, qualifiedColumn(alias, column) + " NOT IN (" + subQuery.build() + ")", subQuery.params());
return this;
}
/**
* 添加 EXISTS 子查询条件。
* <pre>{@code
* QueryBuilder.lambdaJoin("u", SysUser.class)
* .existsSub(SysUserRole.class, sub -> sub
* .selectCountAll()
* .eqColumn(SysUserRole::getUserId, "u", SysUser::getUserId)
* .eq(SysUserRole::getRoleId, roleId))
* .list(SysUserVo.class);
* }</pre>
*
* @param entityClass 子查询实体类型
* @param consumer 子查询构造函数
* @param <Q> 子查询实体类型
* @return 当前联表查询构造辅助对象
*/
public <Q> LambdaJoinQueryBuilder<T> existsSub(Class<Q> entityClass, Consumer<SubQuery<Q>> consumer) {
SubQuery<Q> subQuery = buildPlaceholderSubQuery(entityClass, consumer);
wrapper.apply(true, "EXISTS (" + subQuery.build() + ")", subQuery.params());
return this;
}
/**
* 添加 NOT EXISTS 子查询条件。
*
* @param entityClass 子查询实体类型
* @param consumer 子查询构造函数
* @param <Q> 子查询实体类型
* @return 当前联表查询构造辅助对象
*/
public <Q> LambdaJoinQueryBuilder<T> notExistsSub(Class<Q> entityClass, Consumer<SubQuery<Q>> consumer) {
SubQuery<Q> subQuery = buildPlaceholderSubQuery(entityClass, consumer);
wrapper.apply(true, "NOT EXISTS (" + subQuery.build() + ")", subQuery.params());
return this;
}
/**
* 添加左联表。
*
* @param entityClass 关联实体类型
* @param left 关联实体字段
* @param right 当前查询字段
* @return 当前联表查询构造辅助对象
*/
public <S, X> LambdaJoinQueryBuilder<T> leftJoin(Class<S> entityClass, SFunction<S, ?> left, SFunction<X, ?> right) {
wrapper.leftJoin(entityClass, left, right);
return this;
}
/**
* 添加带别名的左联表。
*
* @param entityClass 关联实体类型
* @param alias 关联表别名
* @param left 关联实体字段
* @param right 当前查询字段
* @return 当前联表查询构造辅助对象
*/
public <S, X> LambdaJoinQueryBuilder<T> leftJoin(Class<S> entityClass, String alias, SFunction<S, ?> left, SFunction<X, ?> right) {
wrapper.leftJoin(entityClass, alias, left, right);
return this;
}
/**
* 添加等于条件。
*
* @param alias 表别名
* @param column 字段
* @param value 条件值
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> eq(String alias, SFunction<S, ?> column, Object value) {
return eq(true, alias, column, value);
}
/**
* 添加等于条件。
*
* @param condition 是否添加该条件
* @param alias 表别名
* @param column 字段
* @param value 条件值
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> eq(boolean condition, String alias, SFunction<S, ?> column, Object value) {
wrapper.eq(condition, alias, column, value);
return this;
}
/**
* 值不为空时添加等于条件。
*
* @param alias 表别名
* @param column 字段
* @param value 条件值
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> eqIfPresent(String alias, SFunction<S, ?> column, Object value) {
return eq(value != null, alias, column, value);
}
/**
* 文本不为空白时添加等于条件。
*
* @param alias 表别名
* @param column 字段
* @param value 条件值
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> eqIfText(String alias, SFunction<S, ?> column, String value) {
return eq(StringUtils.isNotBlank(value), alias, column, value);
}
/**
* 添加不等于条件。
*
* @param alias 表别名
* @param column 字段
* @param value 条件值
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> ne(String alias, SFunction<S, ?> column, Object value) {
return ne(true, alias, column, value);
}
/**
* 添加不等于条件。
*
* @param condition 是否添加该条件
* @param alias 表别名
* @param column 字段
* @param value 条件值
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> ne(boolean condition, String alias, SFunction<S, ?> column, Object value) {
wrapper.ne(condition, alias, column, value);
return this;
}
/**
* 文本不为空白时添加不等于条件。
*
* @param alias 表别名
* @param column 字段
* @param value 条件值
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> neIfText(String alias, SFunction<S, ?> column, String value) {
return ne(StringUtils.isNotBlank(value), alias, column, value);
}
/**
* 添加模糊匹配条件。
*
* @param alias 表别名
* @param column 字段
* @param value 条件值
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> like(String alias, SFunction<S, ?> column, Object value) {
return like(true, alias, column, value);
}
/**
* 添加模糊匹配条件。
*
* @param condition 是否添加该条件
* @param alias 表别名
* @param column 字段
* @param value 条件值
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> like(boolean condition, String alias, SFunction<S, ?> column, Object value) {
wrapper.like(condition, alias, column, value);
return this;
}
/**
* 文本不为空白时添加模糊匹配条件。
*
* @param alias 表别名
* @param column 字段
* @param value 条件值
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> likeIfText(String alias, SFunction<S, ?> column, String value) {
return like(StringUtils.isNotBlank(value), alias, column, value);
}
/**
* 添加区间条件。
*
* @param condition 是否添加该条件
* @param alias 表别名
* @param column 字段
* @param begin 起始值
* @param end 结束值
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> between(boolean condition, String alias, SFunction<S, ?> column, Object begin, Object end) {
wrapper.between(condition, alias, column, begin, end);
return this;
}
/**
* 起止值均不为空时添加区间条件。
*
* @param alias 表别名
* @param column 字段
* @param begin 起始值
* @param end 结束值
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> betweenIfPresent(String alias, SFunction<S, ?> column, Object begin, Object end) {
return between(begin != null && end != null, alias, column, begin, end);
}
/**
* 从参数 Map 中读取起止值,均不为空时添加区间条件。
*
* @param alias 表别名
* @param column 字段
* @param params 参数 Map
* @param beginKey 起始值参数名
* @param endKey 结束值参数名
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> betweenParams(String alias, SFunction<S, ?> column, Map<String, Object> params, String beginKey, String endKey) {
if (params == null) {
return between(false, alias, column, null, null);
}
Object begin = params.get(beginKey);
Object end = params.get(endKey);
return between(begin != null && end != null, alias, column, begin, end);
}
/**
* 添加包含集合条件。
*
* @param condition 是否添加该条件
* @param alias 表别名
* @param column 字段
* @param values 条件值集合
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> in(boolean condition, String alias, SFunction<S, ?> column, Collection<?> values) {
wrapper.in(condition, alias, column, values);
return this;
}
/**
* 添加包含数组条件。
*
* @param condition 是否添加该条件
* @param alias 表别名
* @param column 字段
* @param values 条件值数组
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> in(boolean condition, String alias, SFunction<S, ?> column, Object... values) {
wrapper.in(condition, alias, column, values);
return this;
}
/**
* 添加包含集合条件。
*
* @param alias 表别名
* @param column 字段
* @param values 条件值集合
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> in(String alias, SFunction<S, ?> column, Collection<?> values) {
return in(true, alias, column, values);
}
/**
* 添加包含数组条件。
*
* @param alias 表别名
* @param column 字段
* @param values 条件值数组
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> in(String alias, SFunction<S, ?> column, Object... values) {
return in(true, alias, column, values);
}
/**
* 集合不为空时添加包含条件。
*
* @param alias 表别名
* @param column 字段
* @param values 条件值集合
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> inIfNotEmpty(String alias, SFunction<S, ?> column, Collection<?> values) {
return in(values != null && !values.isEmpty(), alias, column, values);
}
/**
* 数组不为空时添加包含条件。
*
* @param alias 表别名
* @param column 字段
* @param values 条件值数组
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> inIfNotEmpty(String alias, SFunction<S, ?> column, Object... values) {
return in(values != null && values.length > 0, alias, column, values);
}
/**
* 添加不包含集合条件。
*
* @param condition 是否添加该条件
* @param alias 表别名
* @param column 字段
* @param values 条件值集合
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> notIn(boolean condition, String alias, SFunction<S, ?> column, Collection<?> values) {
wrapper.notIn(condition, alias, column, values);
return this;
}
/**
* 集合不为空时添加不包含条件。
*
* @param alias 表别名
* @param column 字段
* @param values 条件值集合
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> notInIfNotEmpty(String alias, SFunction<S, ?> column, Collection<?> values) {
return notIn(values != null && !values.isEmpty(), alias, column, values);
}
/**
* 添加字段非空条件。
*
* @param alias 表别名
* @param column 字段
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> isNotNull(String alias, SFunction<S, ?> column) {
return isNotNull(true, alias, column);
}
/**
* 添加字段非空条件。
*
* @param condition 是否添加该条件
* @param alias 表别名
* @param column 字段
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> isNotNull(boolean condition, String alias, SFunction<S, ?> column) {
wrapper.isNotNull(condition, alias, column);
return this;
}
/**
* 添加升序排序。
*
* @param alias 表别名
* @param column 字段
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> orderByAsc(String alias, SFunction<S, ?> column) {
wrapper.orderByAsc(alias, column);
return this;
}
/**
* 添加降序排序。
*
* @param alias 表别名
* @param column 字段
* @return 当前联表查询构造辅助对象
*/
public <S> LambdaJoinQueryBuilder<T> orderByDesc(String alias, SFunction<S, ?> column) {
wrapper.orderByDesc(alias, column);
return this;
}
/**
* 拼接 SQL 片段条件。
*
* @param condition 是否添加该条件
* @param applySql SQL 片段
* @param values SQL 片段参数
* @return 当前联表查询构造辅助对象
*/
public LambdaJoinQueryBuilder<T> apply(boolean condition, String applySql, Object... values) {
wrapper.apply(condition, applySql, values);
return this;
}
/**
* 添加 FIND_IN_SET 条件。
*
* @param condition 是否添加该条件
* @param value 匹配值
* @param column 字段名
* @return 当前联表查询构造辅助对象
*/
public LambdaJoinQueryBuilder<T> findInSet(boolean condition, Object value, String column) {
return apply(condition, DataBaseHelper.findInSet(value, column));
}
/**
* 使用函数式方式追加 MPJ 原生能力。
*
* @param consumer MPJ 查询包装器消费函数
* @return 当前联表查询构造辅助对象
*/
public LambdaJoinQueryBuilder<T> apply(Consumer<MPJLambdaWrapper<T>> consumer) {
consumer.accept(wrapper);
return this;
}
/**
* 查询列表。
*
* @param resultClass 返回对象类型
* @param <R> 返回对象类型
* @return 查询结果
*/
public <R> List<R> list(Class<R> resultClass) {
return wrapper.list(resultClass);
}
/**
* 分页查询。
*
* @param page 分页对象
* @param resultClass 返回对象类型
* @param <R> 返回对象类型
* @param <P> 分页类型
* @return 分页结果
*/
public <R, P extends IPage<R>> P page(P page, Class<R> resultClass) {
return wrapper.page(page, resultClass);
}
/**
* 查询数量。
*
* @return 数量
*/
public Long count() {
return wrapper.count();
}
/**
* 获取底层 MPJ Lambda 查询包装器。
*
* @return MPJ Lambda 查询包装器
*/
public MPJLambdaWrapper<T> build() {
return wrapper;
}
/**
* 构建使用占位参数模式的子查询。
*
* @param entityClass 子查询实体类型
* @param consumer 子查询构造逻辑
* @param <Q> 子查询实体类型
* @return 子查询构造器
*/
private <Q> SubQuery<Q> buildPlaceholderSubQuery(Class<Q> entityClass, Consumer<SubQuery<Q>> consumer) {
SubQuery<Q> subQuery = SubQuery.ofPlaceholders(entityClass);
consumer.accept(subQuery);
return subQuery;
}
/**
* 解析带表别名的数据库列名。
*
* @param alias 表别名
* @param column 字段引用
* @param <S> 字段所属实体类型
* @return 表别名限定列名
*/
private <S> String qualifiedColumn(String alias, SFunction<S, ?> column) {
return AggregateSelectUtils.checkAlias(alias) + StringPool.DOT + ColumnCache.getMapField(LambdaUtils.getEntityClass(column))
.get(LambdaUtils.getName(column)).getColumn();
}
}
@@ -0,0 +1,443 @@
package org.dromara.common.mybatis.core.query;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.helper.DataBaseHelper;
import java.util.Collection;
import java.util.Map;
/**
* Lambda 查询常用条件扩展。
*
* @param <T> 实体类型
* @param <Children> 链式返回类型
* @author Lion Li
*/
public interface LambdaQueryCondition<T, Children> {
/**
* 等于条件。
*
* @param condition 是否添加该条件
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
Children eq(boolean condition, SFunction<T, ?> column, Object value);
/**
* 不等于条件。
*
* @param condition 是否添加该条件
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
Children ne(boolean condition, SFunction<T, ?> column, Object value);
/**
* 大于条件。
*
* @param condition 是否添加该条件
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
Children gt(boolean condition, SFunction<T, ?> column, Object value);
/**
* 大于等于条件。
*
* @param condition 是否添加该条件
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
Children ge(boolean condition, SFunction<T, ?> column, Object value);
/**
* 小于条件。
*
* @param condition 是否添加该条件
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
Children lt(boolean condition, SFunction<T, ?> column, Object value);
/**
* 小于等于条件。
*
* @param condition 是否添加该条件
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
Children le(boolean condition, SFunction<T, ?> column, Object value);
/**
* 模糊匹配条件。
*
* @param condition 是否添加该条件
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
Children like(boolean condition, SFunction<T, ?> column, Object value);
/**
* 非模糊匹配条件。
*
* @param condition 是否添加该条件
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
Children notLike(boolean condition, SFunction<T, ?> column, Object value);
/**
* 左模糊匹配条件。
*
* @param condition 是否添加该条件
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
Children likeLeft(boolean condition, SFunction<T, ?> column, Object value);
/**
* 右模糊匹配条件。
*
* @param condition 是否添加该条件
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
Children likeRight(boolean condition, SFunction<T, ?> column, Object value);
/**
* 区间条件。
*
* @param condition 是否添加该条件
* @param column 字段
* @param begin 起始值
* @param end 结束值
* @return 链式返回对象
*/
Children between(boolean condition, SFunction<T, ?> column, Object begin, Object end);
/**
* 非区间条件。
*
* @param condition 是否添加该条件
* @param column 字段
* @param begin 起始值
* @param end 结束值
* @return 链式返回对象
*/
Children notBetween(boolean condition, SFunction<T, ?> column, Object begin, Object end);
/**
* 包含集合条件。
*
* @param condition 是否添加该条件
* @param column 字段
* @param values 条件值集合
* @return 链式返回对象
*/
Children in(boolean condition, SFunction<T, ?> column, Collection<?> values);
/**
* 包含数组条件。
*
* @param condition 是否添加该条件
* @param column 字段
* @param values 条件值数组
* @return 链式返回对象
*/
Children in(boolean condition, SFunction<T, ?> column, Object... values);
/**
* 不包含集合条件。
*
* @param condition 是否添加该条件
* @param column 字段
* @param values 条件值集合
* @return 链式返回对象
*/
Children notIn(boolean condition, SFunction<T, ?> column, Collection<?> values);
/**
* 不包含数组条件。
*
* @param condition 是否添加该条件
* @param column 字段
* @param values 条件值数组
* @return 链式返回对象
*/
Children notIn(boolean condition, SFunction<T, ?> column, Object... values);
/**
* 拼接 SQL 片段条件。
*
* @param condition 是否添加该条件
* @param applySql SQL 片段
* @param values SQL 片段参数
* @return 链式返回对象
*/
Children apply(boolean condition, String applySql, Object... values);
/**
* 值不为空时添加等于条件。
*
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
default Children eqIfPresent(SFunction<T, ?> column, Object value) {
return eq(value != null, column, value);
}
/**
* 文本不为空白时添加等于条件。
*
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
default Children eqIfText(SFunction<T, ?> column, String value) {
return eq(StringUtils.isNotBlank(value), column, value);
}
/**
* 值不为空时添加不等于条件。
*
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
default Children neIfPresent(SFunction<T, ?> column, Object value) {
return ne(value != null, column, value);
}
/**
* 文本不为空白时添加不等于条件。
*
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
default Children neIfText(SFunction<T, ?> column, String value) {
return ne(StringUtils.isNotBlank(value), column, value);
}
/**
* 值不为空时添加大于条件。
*
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
default Children gtIfPresent(SFunction<T, ?> column, Object value) {
return gt(value != null, column, value);
}
/**
* 值不为空时添加大于等于条件。
*
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
default Children geIfPresent(SFunction<T, ?> column, Object value) {
return ge(value != null, column, value);
}
/**
* 值不为空时添加小于条件。
*
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
default Children ltIfPresent(SFunction<T, ?> column, Object value) {
return lt(value != null, column, value);
}
/**
* 值不为空时添加小于等于条件。
*
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
default Children leIfPresent(SFunction<T, ?> column, Object value) {
return le(value != null, column, value);
}
/**
* 文本不为空白时添加模糊匹配条件。
*
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
default Children likeIfText(SFunction<T, ?> column, String value) {
return like(StringUtils.isNotBlank(value), column, value);
}
/**
* 文本不为空白时添加非模糊匹配条件。
*
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
default Children notLikeIfText(SFunction<T, ?> column, String value) {
return notLike(StringUtils.isNotBlank(value), column, value);
}
/**
* 文本不为空白时添加左模糊匹配条件。
*
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
default Children likeLeftIfText(SFunction<T, ?> column, String value) {
return likeLeft(StringUtils.isNotBlank(value), column, value);
}
/**
* 文本不为空白时添加右模糊匹配条件。
*
* @param column 字段
* @param value 条件值
* @return 链式返回对象
*/
default Children likeRightIfText(SFunction<T, ?> column, String value) {
return likeRight(StringUtils.isNotBlank(value), column, value);
}
/**
* 起止值均不为空时添加区间条件。
*
* @param column 字段
* @param begin 起始值
* @param end 结束值
* @return 链式返回对象
*/
default Children betweenIfPresent(SFunction<T, ?> column, Object begin, Object end) {
return between(begin != null && end != null, column, begin, end);
}
/**
* 从参数 Map 中读取起止值,均不为空时添加区间条件。
*
* @param column 字段
* @param params 参数 Map
* @param beginKey 起始值参数名
* @param endKey 结束值参数名
* @return 链式返回对象
*/
default Children betweenParams(SFunction<T, ?> column, Map<String, Object> params, String beginKey, String endKey) {
if (params == null) {
return between(false, column, null, null);
}
Object begin = params.get(beginKey);
Object end = params.get(endKey);
return between(begin != null && end != null, column, begin, end);
}
/**
* 起止值均不为空时添加非区间条件。
*
* @param column 字段
* @param begin 起始值
* @param end 结束值
* @return 链式返回对象
*/
default Children notBetweenIfPresent(SFunction<T, ?> column, Object begin, Object end) {
return notBetween(begin != null && end != null, column, begin, end);
}
/**
* 集合不为空时添加包含条件。
*
* @param column 字段
* @param values 条件值集合
* @return 链式返回对象
*/
default Children inIfNotEmpty(SFunction<T, ?> column, Collection<?> values) {
return in(values != null && !values.isEmpty(), column, values);
}
/**
* 数组不为空时添加包含条件。
*
* @param column 字段
* @param values 条件值数组
* @return 链式返回对象
*/
default Children inIfNotEmpty(SFunction<T, ?> column, Object... values) {
return in(values != null && values.length > 0, column, values);
}
/**
* 集合不为空时添加不包含条件。
*
* @param column 字段
* @param values 条件值集合
* @return 链式返回对象
*/
default Children notInIfNotEmpty(SFunction<T, ?> column, Collection<?> values) {
return notIn(values != null && !values.isEmpty(), column, values);
}
/**
* 数组不为空时添加不包含条件。
*
* @param column 字段
* @param values 条件值数组
* @return 链式返回对象
*/
default Children notInIfNotEmpty(SFunction<T, ?> column, Object... values) {
return notIn(values != null && values.length > 0, column, values);
}
/**
* 添加 FIND_IN_SET 条件。
*
* @param value 匹配值
* @param column 字段名
* @return 链式返回对象
*/
default Children findInSet(Object value, String column) {
return findInSet(true, value, column);
}
/**
* 添加 FIND_IN_SET 条件。
*
* @param condition 是否添加该条件
* @param value 匹配值
* @param column 字段名
* @return 链式返回对象
*/
default Children findInSet(boolean condition, Object value, String column) {
return apply(condition, DataBaseHelper.findInSet(value, column));
}
/**
* 值不为空时添加 FIND_IN_SET 条件。
*
* @param value 匹配值
* @param column 字段名
* @return 链式返回对象
*/
default Children findInSetIfPresent(Object value, String column) {
return findInSet(value != null, value, column);
}
}
@@ -0,0 +1,52 @@
package org.dromara.common.mybatis.core.query;
import com.github.yulichang.toolkit.JoinWrappers;
/**
* MyBatis-Plus 查询构造器入口。
*
* @author Lion Li
*/
public final class QueryBuilder {
/**
* 工具入口类不允许实例化。
*/
private QueryBuilder() {
}
/**
* 创建 Lambda 查询构造辅助对象。
*
* @param entityClass 实体类型
* @param <T> 实体类型
* @return Lambda 查询构造辅助对象
*/
public static <T> LambdaQueryBuilder<T> lambda(Class<T> entityClass) {
return new LambdaQueryBuilder<>(new AggregateLambdaQueryWrapper<>(entityClass));
}
/**
* 创建 MPJ Lambda 联表查询构造辅助对象。
*
* @param entityClass 主表实体类型
* @param <T> 主表实体类型
* @return MPJ Lambda 联表查询构造辅助对象
*/
public static <T> LambdaJoinQueryBuilder<T> lambdaJoin(Class<T> entityClass) {
return new LambdaJoinQueryBuilder<>(JoinWrappers.lambda(entityClass));
}
/**
* 创建带主表别名的 MPJ Lambda 联表查询构造辅助对象。
*
* @param alias 主表别名
* @param entityClass 主表实体类型
* @param <T> 主表实体类型
* @return MPJ Lambda 联表查询构造辅助对象
*/
public static <T> LambdaJoinQueryBuilder<T> lambdaJoin(String alias, Class<T> entityClass) {
return new LambdaJoinQueryBuilder<>(JoinWrappers.lambda(alias, entityClass));
}
}
@@ -0,0 +1,59 @@
package org.dromara.common.mybatis.core.query;
/**
* SQL 标准聚合函数。
*
* @author Lion Li
*/
public enum SqlAggregateFunction {
/**
* 求和。
*/
SUM("SUM"),
/**
* 最大值。
*/
MAX("MAX"),
/**
* 最小值。
*/
MIN("MIN"),
/**
* 平均值。
*/
AVG("AVG"),
/**
* 计数。
*/
COUNT("COUNT");
/**
* 聚合函数名称。
*/
private final String name;
/**
* 构造 SQL 聚合函数。
*
* @param name 聚合函数名称
*/
SqlAggregateFunction(String name) {
this.name = name;
}
/**
* 生成聚合函数 SQL 片段。
*
* @param expression 函数入参表达式
* @return 聚合函数 SQL
*/
public String format(String expression) {
return name + "(" + expression + ")";
}
}
@@ -0,0 +1,19 @@
package org.dromara.common.mybatis.core.query;
/**
* SQL 参数格式化器。
*
* @author Lion Li
*/
@FunctionalInterface
public interface SqlParamFormatter {
/**
* 格式化 SQL 参数。
*
* @param value 参数值
* @return MyBatis 参数占位符
*/
String format(Object value);
}
@@ -0,0 +1,531 @@
package org.dromara.common.mybatis.core.query;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.*;
import com.baomidou.mybatisplus.core.toolkit.support.ColumnCache;
import com.baomidou.mybatisplus.core.toolkit.support.LambdaMeta;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import org.apache.ibatis.reflection.property.PropertyNamer;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/**
* Lambda 子查询构造器。
* <p>
* 常用于外层查询的 {@code selectSub}、{@code inSub}、{@code existsSub} 等方法中。
* </p>
* <p>
* 注意:子查询 SQL 由本构造器直接生成,默认会根据 MyBatis-Plus 表元数据追加逻辑删除条件,
* 但不会自动追加项目数据权限条件;如果子查询实体也需要数据权限过滤,请在子查询条件中显式添加。
* </p>
* <pre>{@code
* userMapper.lambda()
* .inSub(SysUser::getUserId, SysUserRole.class, sub -> sub
* .select(SysUserRole::getUserId)
* .eq(SysUserRole::getRoleId, roleId))
* .voList();
*
* userMapper.lambda()
* .select(SysUser::getUserId, SysUser::getUserName)
* .selectSub(SysUserRole.class, sub -> sub
* .selectCountAll()
* .eqColumn(SysUserRole::getUserId, SysUser::getUserId),
* UserStatVo::getRoleCount)
* .voList();
* }</pre>
*
* @param <T> 子查询实体类型
* @author Lion Li
*/
public final class SubQuery<T> {
/**
* 子查询实体类型。
*/
private final Class<T> entityClass;
/**
* 外层查询传入的 SQL 参数格式化器。
*/
private final SqlParamFormatter paramFormatter;
/**
* 是否使用 {@code {0}} 形式的占位参数模式。
*/
private final boolean placeholderParamMode;
/**
* 子查询 SELECT 字段集合。
*/
private final List<String> selects = new ArrayList<>();
/**
* 子查询 WHERE 条件集合。
*/
private final List<String> conditions = new ArrayList<>();
/**
* 占位参数模式下收集的参数值集合。
*/
private final List<Object> params = new ArrayList<>();
/**
* 是否追加逻辑删除条件。
*/
private boolean withLogicDelete = true;
/**
* 构造子查询。
*
* @param entityClass 子查询实体类型
* @param paramFormatter SQL 参数格式化器
*/
private SubQuery(Class<T> entityClass, SqlParamFormatter paramFormatter) {
this(entityClass, paramFormatter, false);
}
/**
* 构造子查询。
*
* @param entityClass 子查询实体类型
* @param paramFormatter SQL 参数格式化器
* @param placeholderParamMode 是否使用占位参数模式
*/
private SubQuery(Class<T> entityClass, SqlParamFormatter paramFormatter, boolean placeholderParamMode) {
this.entityClass = entityClass;
this.paramFormatter = paramFormatter;
this.placeholderParamMode = placeholderParamMode;
}
/**
* 创建子查询。
*
* @param entityClass 子查询实体类型
* @param <T> 子查询实体类型
* @return 子查询构造器
*/
public static <T> SubQuery<T> of(Class<T> entityClass) {
return new SubQuery<>(entityClass, null);
}
/**
* 创建子查询。
*
* @param entityClass 子查询实体类型
* @param paramFormatter SQL 参数格式化器
* @param <T> 子查询实体类型
* @return 子查询构造器
*/
public static <T> SubQuery<T> of(Class<T> entityClass, SqlParamFormatter paramFormatter) {
return new SubQuery<>(entityClass, paramFormatter);
}
/**
* 创建使用 {@code {0}} 参数占位符的子查询。
*
* @param entityClass 子查询实体类型
* @param <T> 子查询实体类型
* @return 子查询构造器
*/
static <T> SubQuery<T> ofPlaceholders(Class<T> entityClass) {
return new SubQuery<>(entityClass, null, true);
}
/**
* 指定子查询字段。
* <pre>{@code
* sub.select(SysUserRole::getUserId)
* }</pre>
*
* @param column 查询字段
* @return 当前子查询构造器
*/
public SubQuery<T> select(SFunction<T, ?> column) {
selects.add(columnName(column));
return this;
}
/**
* 指定 COUNT(*) 查询字段。
* <pre>{@code
* sub.selectCountAll()
* }</pre>
*
* @return 当前子查询构造器
*/
public SubQuery<T> selectCountAll() {
selects.add(SqlAggregateFunction.COUNT.format("*"));
return this;
}
/**
* 指定 SUM 聚合查询字段。
*
* @param column 聚合字段
* @return 当前子查询构造器
*/
public SubQuery<T> selectSum(SFunction<T, ?> column) {
return selectAggregate(SqlAggregateFunction.SUM, column);
}
/**
* 指定 MAX 聚合查询字段。
*
* @param column 聚合字段
* @return 当前子查询构造器
*/
public SubQuery<T> selectMax(SFunction<T, ?> column) {
return selectAggregate(SqlAggregateFunction.MAX, column);
}
/**
* 指定 MIN 聚合查询字段。
*
* @param column 聚合字段
* @return 当前子查询构造器
*/
public SubQuery<T> selectMin(SFunction<T, ?> column) {
return selectAggregate(SqlAggregateFunction.MIN, column);
}
/**
* 指定 AVG 聚合查询字段。
*
* @param column 聚合字段
* @return 当前子查询构造器
*/
public SubQuery<T> selectAvg(SFunction<T, ?> column) {
return selectAggregate(SqlAggregateFunction.AVG, column);
}
/**
* 指定 COUNT 聚合查询字段。
*
* @param column 聚合字段
* @return 当前子查询构造器
*/
public SubQuery<T> selectCount(SFunction<T, ?> column) {
return selectAggregate(SqlAggregateFunction.COUNT, column);
}
/**
* 禁用子查询逻辑删除条件。
* <pre>{@code
* sub.disableLogicDelete()
* }</pre>
*
* @return 当前子查询构造器
*/
public SubQuery<T> disableLogicDelete() {
this.withLogicDelete = false;
return this;
}
/**
* 添加等于条件。
* <pre>{@code
* sub.eq(SysUserRole::getRoleId, roleId)
* }</pre>
*
* @param column 字段
* @param value 条件值
* @return 当前子查询构造器
*/
public SubQuery<T> eq(SFunction<T, ?> column, Object value) {
return condition(column, Constants.EQUALS, value);
}
/**
* 添加大于条件。
*
* @param column 字段
* @param value 条件值
* @return 当前子查询构造器
*/
public SubQuery<T> gt(SFunction<T, ?> column, Object value) {
return condition(column, ">", value);
}
/**
* 添加大于等于条件。
*
* @param column 字段
* @param value 条件值
* @return 当前子查询构造器
*/
public SubQuery<T> ge(SFunction<T, ?> column, Object value) {
return condition(column, ">=", value);
}
/**
* 添加小于条件。
*
* @param column 字段
* @param value 条件值
* @return 当前子查询构造器
*/
public SubQuery<T> lt(SFunction<T, ?> column, Object value) {
return condition(column, "<", value);
}
/**
* 添加小于等于条件。
*
* @param column 字段
* @param value 条件值
* @return 当前子查询构造器
*/
public SubQuery<T> le(SFunction<T, ?> column, Object value) {
return condition(column, "<=", value);
}
/**
* 添加模糊匹配条件。
*
* @param column 字段
* @param value 条件值
* @return 当前子查询构造器
*/
public SubQuery<T> like(SFunction<T, ?> column, Object value) {
return condition(column, "LIKE", "%" + value + "%");
}
/**
* 添加 IN 条件。
* <pre>{@code
* sub.in(SysUserRole::getRoleId, roleIds)
* }</pre>
*
* @param column 字段
* @param values 条件值集合
* @return 当前子查询构造器
*/
public SubQuery<T> in(SFunction<T, ?> column, Collection<?> values) {
if (CollectionUtils.isEmpty(values)) {
return this;
}
conditions.add(columnName(column) + " IN (" + values.stream()
.map(this::formatParam)
.collect(Collectors.joining(Constants.COMMA)) + ")");
return this;
}
/**
* 添加 IN 条件。
*
* @param column 字段
* @param values 条件值数组
* @return 当前子查询构造器
*/
public SubQuery<T> in(SFunction<T, ?> column, Object... values) {
if (values == null || values.length == 0) {
return this;
}
return in(column, Arrays.asList(values));
}
/**
* 添加 BETWEEN 条件。
*
* @param column 字段
* @param begin 起始值
* @param end 结束值
* @return 当前子查询构造器
*/
public SubQuery<T> between(SFunction<T, ?> column, Object begin, Object end) {
conditions.add(columnName(column) + " BETWEEN " + formatParam(begin) + " AND " + formatParam(end));
return this;
}
/**
* 添加字段相等条件,用于关联外层查询字段。
* <pre>{@code
* sub.eqColumn(SysUserRole::getUserId, SysUser::getUserId)
* }</pre>
*
* @param column 子查询字段
* @param otherColumn 其他表字段
* @param <O> 其他表实体类型
* @return 当前子查询构造器
*/
public <O> SubQuery<T> eqColumn(SFunction<T, ?> column, SFunction<O, ?> otherColumn) {
conditions.add(columnName(column) + Constants.EQUALS + qualifiedColumnName(otherColumn));
return this;
}
/**
* 添加字段相等条件,用于关联外层查询字段。
* <pre>{@code
* sub.eqColumn(SysUserRole::getUserId, "u", SysUser::getUserId)
* }</pre>
*
* @param column 子查询字段
* @param tableAlias 其他表别名
* @param otherColumn 其他表字段
* @param <O> 其他表实体类型
* @return 当前子查询构造器
*/
public <O> SubQuery<T> eqColumn(SFunction<T, ?> column, String tableAlias, SFunction<O, ?> otherColumn) {
conditions.add(columnName(column) + Constants.EQUALS + AggregateSelectUtils.checkAlias(tableAlias)
+ StringPool.DOT + columnName(otherColumn));
return this;
}
/**
* 按条件添加子查询条件。
*
* @param condition 是否添加
* @param consumer 子查询条件
* @return 当前子查询构造器
*/
public SubQuery<T> when(boolean condition, Consumer<SubQuery<T>> consumer) {
if (condition) {
consumer.accept(this);
}
return this;
}
/**
* 构建子查询 SQL。
*
* @return 子查询 SQL
*/
public String build() {
Assert.notEmpty(selects, "子查询必须指定查询字段");
String sql = "SELECT " + String.join(Constants.COMMA, selects) + " FROM " + tableName();
List<String> whereConditions = buildWhereConditions();
if (!whereConditions.isEmpty()) {
sql += " WHERE " + String.join(" AND ", whereConditions);
}
return sql;
}
/**
* 获取子查询参数。
*
* @return 子查询参数数组
*/
Object[] params() {
return params.toArray();
}
/**
* 追加聚合查询字段。
*
* @param function 聚合函数
* @param column 聚合字段
* @return 当前子查询构造器
*/
private SubQuery<T> selectAggregate(SqlAggregateFunction function, SFunction<T, ?> column) {
selects.add(function.format(columnName(column)));
return this;
}
/**
* 追加普通比较条件。
*
* @param column 条件字段
* @param operator 比较操作符
* @param value 条件值
* @return 当前子查询构造器
*/
private SubQuery<T> condition(SFunction<T, ?> column, String operator, Object value) {
conditions.add(columnName(column) + StringPool.SPACE + operator + StringPool.SPACE + formatParam(value));
return this;
}
/**
* 格式化 SQL 参数。
*
* @param value 参数值
* @return SQL 参数占位符
*/
private String formatParam(Object value) {
if (placeholderParamMode) {
params.add(value);
return "{" + (params.size() - 1) + "}";
}
Assert.notNull(paramFormatter, "子查询参数需要在外层查询方法中构造");
return paramFormatter.format(value);
}
/**
* 获取子查询实体表名。
*
* @return 表名
*/
private String tableName() {
return tableInfo().getTableName();
}
/**
* 构建最终 WHERE 条件集合。
*
* @return WHERE 条件集合
*/
private List<String> buildWhereConditions() {
List<String> whereConditions = new ArrayList<>();
String logicDeleteSql = logicDeleteSql();
if (StringUtils.isNotBlank(logicDeleteSql)) {
whereConditions.add(logicDeleteSql);
}
whereConditions.addAll(conditions);
return whereConditions;
}
/**
* 获取逻辑删除 SQL 条件。
*
* @return 逻辑删除 SQL 条件,禁用时返回空字符串
*/
private String logicDeleteSql() {
if (!withLogicDelete) {
return StringPool.EMPTY;
}
return tableInfo().getLogicDeleteSql(false, true);
}
/**
* 获取子查询实体对应的 MyBatis-Plus 表信息。
*
* @return 表信息
*/
private TableInfo tableInfo() {
TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
Assert.notNull(tableInfo, "无法获取实体表信息: %s", entityClass.getName());
return tableInfo;
}
/**
* 获取外层查询字段的表名限定列名。
*
* @param column 外层查询字段
* @return 表名限定列名
*/
private String qualifiedColumnName(SFunction<?, ?> column) {
Class<?> columnEntityClass = LambdaUtils.extract(column).getInstantiatedClass();
TableInfo tableInfo = TableInfoHelper.getTableInfo(columnEntityClass);
Assert.notNull(tableInfo, "无法获取实体表信息: %s", columnEntityClass.getName());
return tableInfo.getTableName() + StringPool.DOT + columnName(column);
}
/**
* 从 Lambda 字段引用解析数据库列名。
*
* @param column 字段引用
* @return 数据库列名
*/
private static String columnName(SFunction<?, ?> column) {
LambdaMeta meta = LambdaUtils.extract(column);
String fieldName = PropertyNamer.methodToProperty(meta.getImplMethodName());
Map<String, ColumnCache> columnMap = LambdaUtils.getColumnMap(meta.getInstantiatedClass());
Assert.notNull(columnMap, "can not find lambda cache for this entity [%s]", meta.getInstantiatedClass().getName());
ColumnCache cache = columnMap.get(LambdaUtils.formatKey(fieldName));
Assert.notNull(cache, "can not find lambda cache for this property [%s]", fieldName);
return cache.getColumn();
}
}
@@ -0,0 +1,87 @@
package org.dromara.common.mybatis.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.dromara.common.core.utils.StringUtils;
/**
* 数据库类型
*
* @author Lion Li
*/
@Getter
@AllArgsConstructor
public enum DataBaseType {
/**
* MySQL
*/
MY_SQL("MySQL"),
/**
* Oracle
*/
ORACLE("Oracle"),
/**
* PostgreSQL
*/
POSTGRE_SQL("PostgreSQL"),
/**
* SQL Server
*/
SQL_SERVER("Microsoft SQL Server");
/**
* 数据库类型
*/
private final String type;
/**
* 根据数据库产品名称查找对应的数据库类型
*
* @param databaseProductName 数据库产品名称
* @return 对应的数据库类型枚举值
*/
public static DataBaseType find(String databaseProductName) {
if (StringUtils.isBlank(databaseProductName)) {
return MY_SQL;
}
for (DataBaseType type : values()) {
if (type.getType().equals(databaseProductName)) {
return type;
}
}
return MY_SQL;
}
/**
* 判断是否为 MySQL 类型
*/
public boolean isMySql() {
return this == MY_SQL;
}
/**
* 判断是否为 Oracle 类型
*/
public boolean isOracle() {
return this == ORACLE;
}
/**
* 判断是否为 PostgreSQL 类型
*/
public boolean isPostgreSql() {
return this == POSTGRE_SQL;
}
/**
* 判断是否为 SQL Server 类型
*/
public boolean isSqlServer() {
return this == SQL_SERVER;
}
}
@@ -0,0 +1,90 @@
package org.dromara.common.mybatis.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.helper.DataPermissionHelper;
import org.dromara.system.api.model.LoginUser;
/**
* 数据权限类型枚举
* <p>
* 支持使用 SpEL 模板表达式定义 SQL 查询条件
* 内置数据:
* - {@code user}: 当前登录用户信息,参考 {@link LoginUser}
* 内置服务:
* - {@code sdss}: 系统数据权限服务,参考 ISysDataScopeService
* 如需扩展数据,可以通过 {@link DataPermissionHelper} 进行操作
* 如需扩展服务,可以通过 ISysDataScopeService 自行编写
* </p>
*
* @author Lion Li
* @version 3.5.0
*/
@Getter
@AllArgsConstructor
public enum DataScopeType {
/**
* 全部数据权限
*/
ALL("1", "", ""),
/**
* 自定数据权限
*/
CUSTOM("2", " #{#deptName} IN ( #{@sdss.getRoleCustom( #roleId )} ) ", " 1 = 0 "),
/**
* 部门数据权限
*/
DEPT("3", " #{#deptName} = #{#user.deptId} ", " 1 = 0 "),
/**
* 部门及以下数据权限
*/
DEPT_AND_CHILD("4", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} )", " 1 = 0 "),
/**
* 仅本人数据权限
*/
SELF("5", " #{#userName} = #{#user.userId} ", " 1 = 0 "),
/**
* 部门及以下或本人数据权限
*/
DEPT_AND_CHILD_OR_SELF("6", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} ) OR #{#userName} = #{#user.userId} ", " 1 = 0 ");
/**
* 数据权限类型编码。
*/
private final String code;
/**
* SpEL 模板表达式,用于构建 SQL 查询条件
*/
private final String sqlTemplate;
/**
* 如果不满足 {@code sqlTemplate} 的条件,则使用此默认 SQL 表达式
*/
private final String elseSql;
/**
* 根据枚举代码查找对应的枚举值
*
* @param code 枚举代码
* @return 对应的枚举值,如果未找到则返回 null
*/
public static DataScopeType findCode(String code) {
if (StringUtils.isBlank(code)) {
return null;
}
for (DataScopeType type : values()) {
if (type.getCode().equals(code)) {
return type;
}
}
return null;
}
}
@@ -0,0 +1,115 @@
package org.dromara.common.mybatis.handler;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.HttpStatus;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.ObjectUtils;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.api.model.LoginUser;
import java.time.LocalDateTime;
import java.util.Date;
/**
* MP注入处理器
*
* @author Lion Li
* @date 2021/4/25
*/
@Slf4j
public class InjectionMetaObjectHandler implements MetaObjectHandler {
/**
* 如果用户不存在默认注入-1代表无用户
*/
private static final Long DEFAULT_USER_ID = -1L;
/**
* 插入填充方法,用于在插入数据时自动填充实体对象中的创建时间、更新时间、创建人、更新人等信息
*
* @param metaObject 元对象,用于获取原始对象并进行填充
*/
@Override
public void insertFill(MetaObject metaObject) {
try {
if (ObjectUtil.isNotNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity baseEntity) {
// 获取当前时间作为创建时间和更新时间,如果创建时间不为空,则使用创建时间,否则使用当前时间
LocalDateTime current = ObjectUtils.notNull(baseEntity.getCreateTime(), LocalDateTime.now());
baseEntity.setCreateTime(current);
baseEntity.setUpdateTime(current);
// 如果创建人为空,则填充当前登录用户的信息
if (ObjectUtil.isNull(baseEntity.getCreateBy())) {
LoginUser loginUser = getLoginUser();
if (ObjectUtil.isNotNull(loginUser)) {
Long userId = loginUser.getUserId();
// 填充创建人、更新人和创建部门信息
baseEntity.setCreateBy(userId);
baseEntity.setUpdateBy(userId);
baseEntity.setCreateDept(ObjectUtils.notNull(baseEntity.getCreateDept(), loginUser.getDeptId()));
} else {
// 填充创建人、更新人和创建部门信息
baseEntity.setCreateBy(DEFAULT_USER_ID);
baseEntity.setUpdateBy(DEFAULT_USER_ID);
baseEntity.setCreateDept(ObjectUtils.notNull(baseEntity.getCreateDept(), DEFAULT_USER_ID));
}
}
} else {
LocalDateTime date = LocalDateTime.now();
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, date);
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, date);
Date legacyDate = new Date();
this.strictInsertFill(metaObject, "createTime", Date.class, legacyDate);
this.strictInsertFill(metaObject, "updateTime", Date.class, legacyDate);
}
} catch (Exception e) {
throw new ServiceException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_INTERNAL_ERROR);
}
}
/**
* 更新填充方法,用于在更新数据时自动填充实体对象中的更新时间和更新人信息
*
* @param metaObject 元对象,用于获取原始对象并进行填充
*/
@Override
public void updateFill(MetaObject metaObject) {
try {
if (ObjectUtil.isNotNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity baseEntity) {
// 获取当前时间作为更新时间,无论原始对象中的更新时间是否为空都填充
LocalDateTime current = LocalDateTime.now();
baseEntity.setUpdateTime(current);
// 获取当前登录用户的ID,并填充更新人信息
LoginUser loginUser = getLoginUser();
Long userId = ObjectUtil.isNotNull(loginUser) ? loginUser.getUserId() : DEFAULT_USER_ID;
baseEntity.setUpdateBy(userId);
} else {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
}
} catch (Exception e) {
throw new ServiceException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_INTERNAL_ERROR);
}
}
/**
* 获取当前登录用户信息
*
* @return 当前登录用户的信息,如果用户未登录则返回 null
*/
private LoginUser getLoginUser() {
LoginUser loginUser;
try {
loginUser = LoginHelper.getLoginUser();
} catch (Exception e) {
return null;
}
return loginUser;
}
}
@@ -0,0 +1,97 @@
package org.dromara.common.mybatis.handler;
import cn.dev33.satoken.exception.NotLoginException;
import cn.hutool.http.HttpStatus;
import com.baomidou.dynamic.datasource.exception.CannotFindDataSourceException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R;
import org.mybatis.spring.MyBatisSystemException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* Mybatis异常处理器
*
* @author Lion Li
*/
@Slf4j
@RestControllerAdvice
public class MybatisExceptionHandler {
/**
* 处理主键或唯一索引冲突异常。
*
* @param e 异常信息
* @param request 当前请求
* @return 统一失败响应
*/
@ExceptionHandler(DuplicateKeyException.class)
public R<Void> handleDuplicateKeyException(DuplicateKeyException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',数据库中已存在记录'{}'", requestURI, e.getMessage());
return R.fail(HttpStatus.HTTP_CONFLICT, "数据库中已存在该记录,请联系管理员确认");
}
/**
* 处理 MyBatis 系统异常。
*
* @param e 异常信息
* @param request 当前请求
* @return 统一失败响应
*/
@ExceptionHandler(MyBatisSystemException.class)
public R<Void> handleCannotFindDataSourceException(MyBatisSystemException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
Throwable root = getRootCause(e);
if (root instanceof NotLoginException) {
log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, root.getMessage());
return R.fail(HttpStatus.HTTP_UNAUTHORIZED, "认证失败,无法访问系统资源");
}
if (root instanceof CannotFindDataSourceException) {
log.error("请求地址'{}', 未找到数据源", requestURI);
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, "未找到数据源,请联系管理员确认");
}
log.error("请求地址'{}', Mybatis系统异常", requestURI, e);
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, e.getMessage());
}
/**
* 获取异常的根因(递归查找)
*
* @param e 当前异常
* @return 根因异常(最底层的 cause)
* <p>
* 逻辑说明:
* 1. 如果 e 没有 cause,说明 e 本身就是根因,直接返回
* 2. 如果 e 的 cause 和自身相同(防止循环引用),也返回 e
* 3. 否则递归调用,继续向下寻找最底层的 cause
*/
public static Throwable getRootCause(Throwable e) {
Throwable cause = e.getCause();
if (cause == null || cause == e) {
return e;
}
return getRootCause(cause);
}
/**
* 在异常链中查找指定类型的异常
*
* @param e 当前异常
* @param clazz 目标异常类
* @return 找到的指定类型异常,如果没有找到返回 null
*/
public static Throwable findCause(Throwable e, Class<? extends Throwable> clazz) {
Throwable t = e;
while (t != null && t != t.getCause()) {
if (clazz.isInstance(t)) {
return t;
}
t = t.getCause();
}
return null;
}
}
@@ -0,0 +1,449 @@
package org.dromara.common.mybatis.handler;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaCheckRole;
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.annotation.DataColumn;
import org.dromara.common.mybatis.annotation.DataPermission;
import org.dromara.common.mybatis.core.domain.DataPermissionAccess;
import org.dromara.common.mybatis.enums.DataScopeType;
import org.dromara.common.mybatis.helper.DataPermissionHelper;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.api.domain.RoleDTO;
import org.dromara.system.api.model.LoginUser;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.expression.*;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerMapping;
import java.lang.annotation.Annotation;
import java.util.*;
import java.util.function.Function;
/**
* 数据权限过滤
*
* @author Lion Li
* @version 3.5.0
*/
@Slf4j
public class PlusDataPermissionHandler {
/**
* spel 解析器
*/
private final ExpressionParser parser = new SpelExpressionParser();
/**
* SpEL 模板解析上下文。
*/
private final ParserContext parserContext = new TemplateParserContext();
/**
* bean解析器 用于处理 spel 表达式中对 bean 的调用
*/
private final BeanResolver beanResolver = new BeanFactoryResolver(SpringUtils.getBeanFactory());
/**
* 获取数据过滤条件的 SQL 片段
*
* @param where 原始的查询条件表达式
* @param isSelect 是否为查询语句
* @return 数据过滤条件的 SQL 片段
*/
public Expression getSqlSegment(Expression where, boolean isSelect) {
try {
LoginUser currentUser = currentUser();
// 如果是超级管理员或租户管理员,则不过滤数据
if (LoginHelper.isSuperAdmin()) {
return where;
}
// 构造数据过滤条件的 SQL 片段
String dataFilterSql = buildDataFilter(getDataPermission(), currentUser, isSelect);
if (StringUtils.isBlank(dataFilterSql)) {
return where;
}
Expression expression = CCJSqlParserUtil.parseExpression(dataFilterSql);
// 数据权限使用单独的括号 防止与其他条件冲突
ParenthesedExpressionList<Expression> parenthesis = new ParenthesedExpressionList<>(expression);
if (ObjectUtil.isNotNull(where)) {
return new AndExpression(where, parenthesis);
} else {
return parenthesis;
}
} catch (JSQLParserException e) {
throw new ServiceException("数据权限解析异常 => " + e.getMessage());
} finally {
DataPermissionHelper.removePermission();
}
}
/**
* 构建数据过滤条件的 SQL 语句
*
* @param dataPermission 数据权限注解
* @param isSelect 标志当前操作是否为查询操作,查询操作和更新或删除操作在处理过滤条件时会有不同的处理方式
* @return 构建的数据过滤条件的 SQL 语句
* @throws ServiceException 如果角色的数据范围异常或者 key 与 value 的长度不匹配,则抛出 ServiceException 异常
*/
private String buildDataFilter(DataPermission dataPermission, LoginUser user, boolean isSelect) {
// 更新或删除需满足所有条件
String joinStr = isSelect ? " OR " : " AND ";
if (StringUtils.isNotBlank(dataPermission.joinStr())) {
joinStr = " " + dataPermission.joinStr() + " ";
}
Object defaultValue = "-1";
NullSafeStandardEvaluationContext context = new NullSafeStandardEvaluationContext(defaultValue);
context.addPropertyAccessor(new NullSafePropertyAccessor(context.getPropertyAccessors().getFirst(), defaultValue));
context.setBeanResolver(beanResolver);
DataPermissionHelper.getContext().forEach(context::setVariable);
Set<String> conditions = new HashSet<>();
DataPermissionAccess access = currentAccess();
List<RoleDTO> scopeRoles = scopeRoles(user, access);
if (CollUtil.isEmpty(scopeRoles)) {
if (access.constrained()) {
return " 1 = 0 ";
}
return StringUtils.EMPTY;
}
// 优先设置变量
List<String> keys = new ArrayList<>();
for (DataColumn dataColumn : dataPermission.value()) {
if (dataColumn.key().length != dataColumn.value().length) {
throw new ServiceException("角色数据范围异常 => key与value长度不匹配");
}
// 设置注解变量 key 为表达式变量 value 为变量值
for (int i = 0; i < dataColumn.key().length; i++) {
context.setVariable(dataColumn.key()[i], dataColumn.value()[i]);
}
keys.addAll(Arrays.stream(dataColumn.key()).map(key -> "#" + key).toList());
}
for (RoleDTO role : scopeRoles) {
context.setVariable("roleId", role.getRoleId());
// 获取角色权限泛型
DataScopeType type = DataScopeType.findCode(role.getDataScope());
if (ObjectUtil.isNull(type)) {
throw new ServiceException("角色数据范围异常 => " + role.getDataScope());
}
// 全部数据权限直接返回
if (type == DataScopeType.ALL) {
return StringUtils.EMPTY;
}
boolean isSuccess = false;
for (DataColumn dataColumn : dataPermission.value()) {
// 不包含 key 变量 则不处理
if (!StringUtils.containsAny(type.getSqlTemplate(), keys.toArray(String[]::new))) {
continue;
}
// 当前注解不满足模板 不处理
if (!StringUtils.containsAny(type.getSqlTemplate(), dataColumn.key())) {
continue;
}
// 忽略数据权限 防止spel表达式内有其他sql查询导致死循环调用
String sql = DataPermissionHelper.ignore(() ->
parser.parseExpression(type.getSqlTemplate(), parserContext).getValue(context, String.class)
);
// 解析sql模板并填充
conditions.add(joinStr + sql);
isSuccess = true;
}
// 未处理成功则填充兜底方案
if (!isSuccess && StringUtils.isNotBlank(type.getElseSql())) {
conditions.add(joinStr + type.getElseSql());
}
}
if (CollUtil.isNotEmpty(conditions)) {
String sql = StreamUtils.join(conditions, Function.identity(), "");
return sql.substring(joinStr.length());
}
return StringUtils.EMPTY;
}
/**
* 获取当前登录用户信息
*
* @return 当前登录用户的LoginUser对象,可能为null(如未登录场景)
*/
private LoginUser currentUser() {
// 从数据权限助手缓存中获取当前登录用户
LoginUser currentUser = DataPermissionHelper.getVariable("user");
if (ObjectUtil.isNull(currentUser)) {
currentUser = LoginHelper.getLoginUser();
DataPermissionHelper.setVariable("user", currentUser);
}
return currentUser;
}
/**
* 获取当前请求已解析的数据权限访问控制对象。
*
* @return 数据权限访问控制对象
*/
private DataPermissionAccess currentAccess() {
DataPermissionAccess access = DataPermissionHelper.getAccess();
if (access != null) {
return access;
}
DataPermissionAccess resolvedAccess = resolveAccess();
DataPermissionHelper.setAccess(resolvedAccess);
return resolvedAccess;
}
/**
* 根据当前接口权限约束筛选参与数据权限计算的角色。
*
* @param user 当前登录用户
* @param access 当前接口访问约束
* @return 参与数据权限计算的角色集合
*/
private List<RoleDTO> scopeRoles(LoginUser user, DataPermissionAccess access) {
List<RoleDTO> roles = user.getRoles();
if (!access.constrained()) {
return roles;
}
Map<Long, RoleDTO> allRoleMap = new LinkedHashMap<>();
if (CollUtil.isNotEmpty(roles)) {
roles.forEach(role -> allRoleMap.put(role.getRoleId(), role));
}
Map<Long, RoleDTO> roleMap = new LinkedHashMap<>();
Map<String, List<Long>> dataScopeRoleMap = user.getDataScopeRoleMap();
if (CollUtil.isNotEmpty(dataScopeRoleMap)) {
access.perms().forEach(perm -> {
List<Long> roleIds = dataScopeRoleMap.get(perm);
if (CollUtil.isNotEmpty(roleIds)) {
roleIds.forEach(roleId -> {
RoleDTO role = allRoleMap.get(roleId);
if (role != null) {
roleMap.putIfAbsent(role.getRoleId(), role);
}
});
}
});
}
if (CollUtil.isNotEmpty(roles) && CollUtil.isNotEmpty(access.roleKeys())) {
roles.stream()
.filter(role -> StringUtils.isNotBlank(role.getRoleKey()))
.filter(role -> StringUtils.splitList(role.getRoleKey()).stream().anyMatch(access.roleKeys()::contains))
.forEach(role -> roleMap.putIfAbsent(role.getRoleId(), role));
}
return new ArrayList<>(roleMap.values());
}
/**
* 从当前请求处理器上解析接口权限和角色约束。
*
* @return 数据权限访问控制对象
*/
private DataPermissionAccess resolveAccess() {
HttpServletRequest request = ServletUtils.getRequest();
if (request == null) {
return DataPermissionAccess.EMPTY;
}
Object handler = request.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE);
if (!(handler instanceof HandlerMethod handlerMethod)) {
return DataPermissionAccess.EMPTY;
}
Set<String> perms = new LinkedHashSet<>();
Set<String> roleKeys = new LinkedHashSet<>();
SaCheckPermission saCheckPermission = findAnnotation(handlerMethod, SaCheckPermission.class);
if (saCheckPermission != null) {
perms.addAll(toSet(saCheckPermission.value()));
roleKeys.addAll(toSet(saCheckPermission.orRole()));
}
SaCheckRole saCheckRole = findAnnotation(handlerMethod, SaCheckRole.class);
if (saCheckRole != null) {
roleKeys.addAll(toSet(saCheckRole.value()));
}
if (perms.isEmpty() && roleKeys.isEmpty()) {
return DataPermissionAccess.EMPTY;
}
return new DataPermissionAccess(Set.copyOf(perms), Set.copyOf(roleKeys));
}
/**
* 优先从方法、再从类上查找指定注解。
*
* @param handlerMethod 当前请求处理方法
* @param annotationType 注解类型
* @param <A> 注解类型
* @return 注解对象,未配置时返回 null
*/
private <A extends Annotation> A findAnnotation(HandlerMethod handlerMethod, Class<A> annotationType) {
A annotation = AnnotationUtil.getAnnotation(handlerMethod.getMethod(), annotationType);
if (annotation != null) {
return annotation;
}
return AnnotationUtil.getAnnotation(handlerMethod.getBeanType(), annotationType);
}
/**
* 将注解中的字符串数组转换为去空后的集合。
*
* @param values 注解值数组
* @return 字符串集合
*/
private Set<String> toSet(String[] values) {
if (values == null || values.length == 0) {
return Set.of();
}
Set<String> result = new LinkedHashSet<>();
Arrays.stream(values).filter(StringUtils::isNotBlank).forEach(result::add);
return result;
}
/**
* 根据映射语句 ID 或类名获取对应的 DataPermission 注解对象
*
* @return DataPermission 注解对象,如果不存在则返回 null
*/
public DataPermission getDataPermission() {
return DataPermissionHelper.getPermission();
}
/**
* 检查给定的映射语句 ID 是否有效,即是否能够找到对应的 DataPermission 注解对象
*
* @return 如果找到对应的 DataPermission 注解对象,则返回 false;否则返回 true
*/
public boolean invalid() {
return getDataPermission() == null;
}
/**
* 对所有null变量找不到的变量返回默认值
*/
@AllArgsConstructor
private static class NullSafeStandardEvaluationContext extends StandardEvaluationContext {
/**
* 变量值为空时返回的默认值。
*/
private final Object defaultValue;
/**
* 查找 SpEL 变量,变量为空时返回默认值。
*
* @param name 变量名
* @return 变量值或默认值
*/
@Override
public Object lookupVariable(String name) {
Object obj = super.lookupVariable(name);
// 如果读取到的值是 null,则返回默认值
if (obj == null) {
return defaultValue;
}
return obj;
}
}
/**
* 对所有null变量找不到的变量返回默认值 委托模式 将不需要处理的方法委托给原处理器
*/
@AllArgsConstructor
private static class NullSafePropertyAccessor implements PropertyAccessor {
/**
* 原始属性访问器。
*/
private final PropertyAccessor delegate;
/**
* 属性值为空时返回的默认值。
*/
private final Object defaultValue;
/**
* 获取当前访问器支持的目标类型。
*
* @return 目标类型数组
*/
@Override
public Class<?>[] getSpecificTargetClasses() {
return delegate.getSpecificTargetClasses();
}
/**
* 判断指定属性是否可读。
*
* @param context 表达式上下文
* @param target 目标对象
* @param name 属性名
* @return true 可读 false 不可读
* @throws AccessException 属性访问异常
*/
@Override
public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
return delegate.canRead(context, target, name);
}
/**
* 读取属性值,属性值为空时返回默认值。
*
* @param context 表达式上下文
* @param target 目标对象
* @param name 属性名
* @return 属性值
* @throws AccessException 属性访问异常
*/
@Override
public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException {
TypedValue value = delegate.read(context, target, name);
// 如果读取到的值是 null,则返回默认值
if (value.getValue() == null) {
return new TypedValue(defaultValue);
}
return value;
}
/**
* 判断指定属性是否可写。
*
* @param context 表达式上下文
* @param target 目标对象
* @param name 属性名
* @return true 可写 false 不可写
* @throws AccessException 属性访问异常
*/
@Override
public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException {
return delegate.canWrite(context, target, name);
}
/**
* 写入属性值。
*
* @param context 表达式上下文
* @param target 目标对象
* @param name 属性名
* @param newValue 新属性值
* @throws AccessException 属性访问异常
*/
@Override
public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException {
delegate.write(context, target, name, newValue);
}
}
}
@@ -0,0 +1,35 @@
package org.dromara.common.mybatis.handler;
import cn.hutool.core.convert.Convert;
import com.baomidou.mybatisplus.core.handlers.PostInitTableInfoHandler;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.session.Configuration;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.reflect.ReflectUtils;
/**
* 修改表信息初始化方式
* 目前用于全局修改是否使用逻辑删除
*
* @author Lion Li
*/
public class PlusPostInitTableInfoHandler implements PostInitTableInfoHandler {
/**
* 表信息初始化后统一调整逻辑删除开关。
*
* @param tableInfo 表信息
* @param configuration MyBatis 配置
* @return 调整后的表信息
*/
@Override
public TableInfo postTableInfo(TableInfo tableInfo, Configuration configuration) {
String flag = SpringUtils.getProperty("mybatis-plus.enableLogicDelete", "true");
// 只有关闭时 统一设置false 为true时mp自动判断不处理
if (!Convert.toBool(flag)) {
ReflectUtils.setFieldValue(tableInfo, "withLogicDelete", false);
}
return tableInfo;
}
}
@@ -0,0 +1,122 @@
package org.dromara.common.mybatis.helper;
import cn.hutool.core.convert.Convert;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.sql.SqlUtil;
import org.dromara.common.mybatis.enums.DataBaseType;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 数据库助手
*
* @author Lion Li
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class DataBaseHelper {
/**
* 动态数据源路由对象。
*/
private static final DynamicRoutingDataSource DS = SpringUtils.getBean(DynamicRoutingDataSource.class);
/**
* 数据源对应数据库类型缓存。
*/
private static final Map<String, DataBaseType> DB_TYPE_CACHE = new ConcurrentHashMap<>();
/**
* 获取当前数据源对应的数据库类型
* <p>
* 通过 DynamicRoutingDataSource 获取当前线程绑定的数据源,
* 然后从数据源获取数据库连接,利用连接的元数据获取数据库产品名称,
* 最后调用 DataBaseType.find 方法将数据库名称转换为对应的枚举类型
*
* @return 当前数据库对应的 DataBaseType 枚举,找不到时默认返回 MY_SQL
* @throws ServiceException 当获取数据库连接或元数据出现异常时抛出业务异常
*/
public static DataBaseType getDataBaseType() {
DataSource dataSource = DS.determineDataSource();
String dsKey = DynamicDataSourceContextHolder.peek();
final String key = dsKey != null ? dsKey : "primary";
return DB_TYPE_CACHE.computeIfAbsent(key, k -> {
try (Connection conn = dataSource.getConnection()) {
DatabaseMetaData metaData = conn.getMetaData();
String databaseProductName = metaData.getDatabaseProductName();
return DataBaseType.find(databaseProductName);
} catch (SQLException e) {
throw new RuntimeException("获取数据库类型失败", e);
}
});
}
/**
* 获取指定数据源对应的数据库类型
*
* @param dsName 数据源名称
* @return 指定数据库对应的 DataBaseType 枚举,找不到时默认返回 MY_SQL
* @throws ServiceException 当获取数据库连接或元数据出现异常时抛出业务异常
*/
public static DataBaseType getDataBaseType(String dsName) {
DataSource dataSource = DS.getDataSource(dsName);
return DB_TYPE_CACHE.computeIfAbsent(dsName, k -> {
try (Connection conn = dataSource.getConnection()) {
DatabaseMetaData metaData = conn.getMetaData();
String databaseProductName = metaData.getDatabaseProductName();
return DataBaseType.find(databaseProductName);
} catch (SQLException e) {
throw new RuntimeException("获取数据库类型失败", e);
}
});
}
/**
* 根据当前数据库类型,生成兼容的 FIND_IN_SET 语句片段
* <p>
* 用于判断指定值是否存在于逗号分隔的字符串列中,SQL写法根据不同数据库方言自动切换:
* - Oracle 使用 instr 函数
* - PostgreSQL 使用 strpos 函数
* - SQL Server 使用 charindex 函数
* - 其他默认使用 MySQL 的 find_in_set 函数
*
* @param var1 要查找的值(支持任意类型,内部会转换成字符串)
* @param var2 存储逗号分隔值的数据库列名
* @return 适用于当前数据库的 SQL 条件字符串,通常用于 where 或 apply 中拼接
*/
public static String findInSet(Object var1, String var2) {
String var = Convert.toStr(var1);
SqlUtil.filterKeyword(var);
SqlUtil.filterKeyword(var2);
return switch (getDataBaseType()) {
// instr(',0,100,101,' , ',100,') <> 0
case ORACLE -> "instr(','||%s||',' , ',%s,') <> 0".formatted(var2, var);
// (select strpos(',0,100,101,' , ',100,')) <> 0
case POSTGRE_SQL -> "(select strpos(','||%s||',' , ',%s,')) <> 0".formatted(var2, var);
// charindex(',100,' , ',0,100,101,') <> 0
case SQL_SERVER -> "charindex(',%s,' , ','+%s+',') <> 0".formatted(var, var2);
// find_in_set(100 , '0,100,101')
default -> "find_in_set('%s' , %s) <> 0".formatted(var, var2);
};
}
/**
* 获取当前注册的数据源名称列表。
*
* @return 数据源名称列表
*/
public static List<String> getDataSourceNameList() {
return new ArrayList<>(DS.getDataSources().keySet());
}
}
@@ -0,0 +1,154 @@
package org.dromara.common.mybatis.helper;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaStorage;
import cn.hutool.core.util.ObjectUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.mybatis.annotation.DataPermission;
import org.dromara.common.mybatis.core.domain.DataPermissionAccess;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
/**
* 数据权限助手
*
* @author Lion Li
* @version 3.5.0
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings("unchecked")
public class DataPermissionHelper {
/**
* Sa-Token Storage 中保存数据权限上下文的键。
*/
private static final String DATA_PERMISSION_KEY = "data:permission";
/**
* 数据权限访问控制对象在上下文中的键。
*/
private static final String ACCESS_KEY = "data:permission:access";
/**
* 当前线程正在执行的 Mapper 数据权限注解缓存。
*/
private static final ThreadLocal<DataPermission> PERMISSION_CACHE = new ThreadLocal<>();
/**
* 获取当前执行mapper权限注解
*
* @return 返回当前执行mapper权限注解
*/
public static DataPermission getPermission() {
return PERMISSION_CACHE.get();
}
/**
* 设置当前执行mapper权限注解
*
* @param dataPermission 数据权限注解
*/
public static void setPermission(DataPermission dataPermission) {
PERMISSION_CACHE.set(dataPermission);
}
/**
* 删除当前执行mapper权限注解
*/
public static void removePermission() {
PERMISSION_CACHE.remove();
}
/**
* 从上下文中获取指定键的变量值,并将其转换为指定的类型
*
* @param key 变量的键
* @param <T> 变量值的类型
* @return 指定键的变量值,如果不存在则返回 null
*/
public static <T> T getVariable(String key) {
Map<String, Object> context = getContext();
return (T) context.get(key);
}
/**
* 向上下文中设置指定键的变量值
*
* @param key 要设置的变量的键
* @param value 要设置的变量值
*/
public static void setVariable(String key, Object value) {
Map<String, Object> context = getContext();
context.put(key, value);
}
/**
* 获取当前数据权限访问控制对象。
*
* @return 访问控制对象
*/
public static DataPermissionAccess getAccess() {
return getVariable(ACCESS_KEY);
}
/**
* 设置当前数据权限访问控制对象。
*
* @param access 访问控制对象
*/
public static void setAccess(DataPermissionAccess access) {
setVariable(ACCESS_KEY, access);
}
/**
* 获取数据权限上下文
*
* @return 存储在SaStorage中的Map对象,用于存储数据权限相关的上下文信息
* @throws NullPointerException 如果数据权限上下文类型异常,则抛出NullPointerException
*/
public static Map<String, Object> getContext() {
SaStorage saStorage = SaHolder.getStorage();
Object attribute = saStorage.get(DATA_PERMISSION_KEY);
if (ObjectUtil.isNull(attribute)) {
saStorage.set(DATA_PERMISSION_KEY, new HashMap<>());
attribute = saStorage.get(DATA_PERMISSION_KEY);
}
if (attribute instanceof Map map) {
return map;
}
throw new IllegalStateException("data permission context type exception");
}
/**
* 在忽略数据权限中执行
*
* @param handle 处理执行方法
*/
public static void ignore(Runnable handle) {
DataPermissionIgnoreContext.enable();
try {
handle.run();
} finally {
DataPermissionIgnoreContext.disable();
}
}
/**
* 在忽略数据权限中执行
*
* @param handle 处理执行方法
* @return 执行结果
*/
public static <T> T ignore(Supplier<T> handle) {
DataPermissionIgnoreContext.enable();
try {
return handle.get();
} finally {
DataPermissionIgnoreContext.disable();
}
}
}
@@ -0,0 +1,87 @@
package org.dromara.common.mybatis.helper;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.reflect.ReflectUtils;
import java.util.ArrayDeque;
import java.util.Deque;
/**
* 数据权限忽略状态适配器。
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
final class DataPermissionIgnoreContext {
/**
* 数据权限忽略状态栈,用于支持嵌套忽略并恢复进入前状态。
*/
private static final ThreadLocal<Deque<Boolean>> DATA_PERMISSION_STACK = ThreadLocal.withInitial(ArrayDeque::new);
/**
* 开启忽略数据权限。
*/
static void enable() {
IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
DATA_PERMISSION_STACK.get().push(ignoreStrategy != null && Boolean.TRUE.equals(ignoreStrategy.getDataPermission()));
if (ObjectUtil.isNull(ignoreStrategy)) {
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().dataPermission(true).build());
} else {
ignoreStrategy.setDataPermission(true);
}
}
/**
* 关闭忽略数据权限,并恢复进入前的数据权限忽略状态。
*/
static void disable() {
Deque<Boolean> stack = DATA_PERMISSION_STACK.get();
boolean previousDataPermission = !stack.isEmpty() && stack.pop();
IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
if (ObjectUtil.isNotNull(ignoreStrategy)) {
if (previousDataPermission) {
ignoreStrategy.setDataPermission(true);
} else if (isOnlyDataPermissionIgnored(ignoreStrategy) && stack.isEmpty()) {
InterceptorIgnoreHelper.clearIgnoreStrategy();
} else {
ignoreStrategy.setDataPermission(false);
}
}
if (stack.isEmpty()) {
DATA_PERMISSION_STACK.remove();
}
}
/**
* 获取 MyBatis-Plus 当前线程中的拦截器忽略策略。
*
* @return 当前忽略策略,未设置时返回 null
*/
private static IgnoreStrategy getIgnoreStrategy() {
Object ignoreStrategyLocal = ReflectUtils.getStaticFieldValue(ReflectUtils.getField(InterceptorIgnoreHelper.class, "IGNORE_STRATEGY_LOCAL"));
if (ignoreStrategyLocal instanceof ThreadLocal<?> ignoreStrategyThreadLocal
&& ignoreStrategyThreadLocal.get() instanceof IgnoreStrategy ignoreStrategy) {
return ignoreStrategy;
}
return null;
}
/**
* 判断当前忽略策略是否只忽略了数据权限插件。
*
* @param ignoreStrategy 忽略策略
* @return true 仅忽略数据权限 false 还忽略了其他插件能力
*/
private static boolean isOnlyDataPermissionIgnored(IgnoreStrategy ignoreStrategy) {
return !Boolean.TRUE.equals(ignoreStrategy.getDynamicTableName())
&& !Boolean.TRUE.equals(ignoreStrategy.getBlockAttack())
&& !Boolean.TRUE.equals(ignoreStrategy.getIllegalSql())
&& !Boolean.TRUE.equals(ignoreStrategy.getTenantLine())
&& CollectionUtil.isEmpty(ignoreStrategy.getOthers());
}
}
@@ -0,0 +1,174 @@
package org.dromara.common.mybatis.interceptor;
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.BaseMultiTableInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SetOperationList;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.dromara.common.mybatis.handler.PlusDataPermissionHandler;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
/**
* 数据权限拦截器
*
* @author Lion Li
* @version 3.5.0
*/
@Slf4j
public class PlusDataPermissionInterceptor extends BaseMultiTableInnerInterceptor implements InnerInterceptor {
/**
* 数据权限 SQL 处理器。
*/
private final PlusDataPermissionHandler dataPermissionHandler = new PlusDataPermissionHandler();
/**
* 在执行查询之前,检查并处理数据权限相关逻辑
*
* @param executor MyBatis 执行器对象
* @param ms 映射语句对象
* @param parameter 方法参数
* @param rowBounds 分页对象
* @param resultHandler 结果处理器
* @param boundSql 绑定的 SQL 对象
* @throws SQLException 如果发生 SQL 异常
*/
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
// 检查是否需要忽略数据权限处理
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
return;
}
// 检查是否缺少有效的数据权限注解
if (dataPermissionHandler.invalid()) {
return;
}
// 解析 sql 分配对应方法
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
}
/**
* 在准备 SQL 语句之前,检查并处理更新和删除操作的数据权限相关逻辑
*
* @param sh MyBatis StatementHandler 对象
* @param connection 数据库连接对象
* @param transactionTimeout 事务超时时间
*/
@Override
public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
MappedStatement ms = mpSh.mappedStatement();
// 获取 SQL 命令类型(增、删、改、查)
SqlCommandType sct = ms.getSqlCommandType();
// 只处理更新和删除操作的 SQL 语句
if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
return;
}
// 检查是否缺少有效的数据权限注解
if (dataPermissionHandler.invalid()) {
return;
}
PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
mpBs.sql(parserMulti(mpBs.sql(), ms.getId()));
}
}
/**
* 处理 SELECT 查询语句中的 WHERE 条件
*
* @param select SELECT 查询对象
* @param index 查询语句的索引
* @param sql 查询语句
* @param obj WHERE 条件参数
*/
@Override
protected void processSelect(Select select, int index, String sql, Object obj) {
if (select instanceof PlainSelect) {
this.setWhere((PlainSelect) select, (String) obj);
} else if (select instanceof SetOperationList setOperationList) {
List<Select> selectBodyList = setOperationList.getSelects();
selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
}
}
/**
* 处理 UPDATE 语句中的 WHERE 条件
*
* @param update UPDATE 查询对象
* @param index 查询语句的索引
* @param sql 查询语句
* @param obj WHERE 条件参数
*/
@Override
protected void processUpdate(Update update, int index, String sql, Object obj) {
Expression sqlSegment = dataPermissionHandler.getSqlSegment(update.getWhere(), false);
if (null != sqlSegment) {
update.setWhere(sqlSegment);
}
}
/**
* 处理 DELETE 语句中的 WHERE 条件
*
* @param delete DELETE 查询对象
* @param index 查询语句的索引
* @param sql 查询语句
* @param obj WHERE 条件参数
*/
@Override
protected void processDelete(Delete delete, int index, String sql, Object obj) {
Expression sqlSegment = dataPermissionHandler.getSqlSegment(delete.getWhere(), false);
if (null != sqlSegment) {
delete.setWhere(sqlSegment);
}
}
/**
* 设置 SELECT 语句的 WHERE 条件
*
* @param plainSelect SELECT 查询对象
* @param mappedStatementId 映射语句的 ID
*/
protected void setWhere(PlainSelect plainSelect, String mappedStatementId) {
Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), true);
if (null != sqlSegment) {
plainSelect.setWhere(sqlSegment);
}
}
/**
* 构建表达式,用于处理表的数据权限
*
* @param table 表对象
* @param where WHERE 条件表达式
* @param whereSegment WHERE 条件片段
* @return 构建的表达式
*/
@Override
public Expression buildTableExpression(Table table, Expression where, String whereSegment) {
// 只有新版数据权限处理器才会执行到这里
final MultiDataPermissionHandler handler = (MultiDataPermissionHandler) dataPermissionHandler;
return handler.getSqlSegment(table, where, whereSegment);
}
}
@@ -0,0 +1,361 @@
package org.dromara.common.mybatis.interceptor;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.dromara.common.mybatis.config.properties.SqlLogProperties;
import java.lang.reflect.InvocationTargetException;
import java.sql.Statement;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.TemporalAccessor;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantLock;
/**
* 完整 SQL 日志拦截器。
*
* @author Lion Li
*/
@Slf4j(topic = "SQL_FULL")
@Intercepts({
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
@Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
@Signature(type = StatementHandler.class, method = "batch", args = {Statement.class}),
@Signature(type = StatementHandler.class, method = "queryCursor", args = {Statement.class})
})
public class SqlLogInterceptor implements Interceptor {
/**
* 单条日志分片长度。
*/
private static final int CHUNK_SIZE = 8000;
/**
* SQL 空白字符匹配。
*/
private static final String BLANK_REGEX = "\\s+";
/**
* 日期时间格式。
*/
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
* 日期格式。
*/
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
/**
* 时间格式。
*/
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
/**
* 控制台输出锁,避免多线程 SQL 日志互相穿插。
*/
private static final ReentrantLock CONSOLE_LOCK = new ReentrantLock();
/**
* SQL 日志配置。
*/
private final SqlLogProperties sqlLogProperties;
public SqlLogInterceptor(SqlLogProperties sqlLogProperties) {
this.sqlLogProperties = sqlLogProperties;
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
BoundSql boundSql = statementHandler.getBoundSql();
MappedStatement mappedStatement = PluginUtils.mpStatementHandler(statementHandler).mappedStatement();
long startTime = System.currentTimeMillis();
try {
Object result = invocation.proceed();
printSql(mappedStatement, boundSql, System.currentTimeMillis() - startTime, null);
return result;
} catch (Throwable e) {
printSql(mappedStatement, boundSql, System.currentTimeMillis() - startTime, e);
throw e;
}
}
/**
* 输出 SQL。
*
* @param mappedStatement 映射语句
* @param boundSql 绑定 SQL
* @param elapsedTime 执行耗时
* @param throwable 执行异常
*/
private void printSql(MappedStatement mappedStatement, BoundSql boundSql, long elapsedTime, Throwable throwable) {
String fullSql = buildFullSql(mappedStatement.getConfiguration(), boundSql);
String message = buildLogMessage(mappedStatement, elapsedTime, fullSql, throwable);
printChunk(message);
}
/**
* 构建日志内容。
*
* @param mappedStatement 映射语句
* @param elapsedTime 执行耗时
* @param fullSql 完整 SQL
* @param throwable 执行异常
* @return 日志内容
*/
private String buildLogMessage(MappedStatement mappedStatement, long elapsedTime, String fullSql, Throwable throwable) {
if (isLogOutput()) {
String message = StrUtil.format("Consume Time{} ms {} Mapper ID{} Execute SQL{}",
elapsedTime, DateUtil.now(), mappedStatement.getId(), fullSql);
String errorMessage = formatThrowable(throwable);
if (StrUtil.isNotBlank(errorMessage)) {
message = message + " Execute Error" + errorMessage;
}
return message;
}
String message = StrUtil.format("Consume Time{} ms {}\nMapper ID{}\nExecute SQL{}",
elapsedTime, DateUtil.now(), mappedStatement.getId(), fullSql);
String errorMessage = formatThrowable(throwable);
if (StrUtil.isNotBlank(errorMessage)) {
message = message + "\nExecute Error" + errorMessage;
}
return message;
}
/**
* 格式化执行异常。
*
* @param throwable 执行异常
* @return 异常信息
*/
private String formatThrowable(Throwable throwable) {
if (throwable == null) {
return StrUtil.EMPTY;
}
Throwable realThrowable = unwrapThrowable(throwable);
String message = realThrowable.getMessage();
if (StrUtil.isBlank(message)) {
return StrUtil.EMPTY;
}
return realThrowable.getClass().getName() + ": " + message;
}
/**
* 解包反射调用异常。
*
* @param throwable 执行异常
* @return 实际异常
*/
private Throwable unwrapThrowable(Throwable throwable) {
if (throwable instanceof InvocationTargetException invocationTargetException
&& invocationTargetException.getTargetException() != null) {
return invocationTargetException.getTargetException();
}
return throwable;
}
/**
* 构建完整 SQL。
*
* @param configuration MyBatis 配置
* @param boundSql 绑定 SQL
* @return 完整 SQL
*/
private String buildFullSql(Configuration configuration, BoundSql boundSql) {
String sql = boundSql.getSql().replaceAll(BLANK_REGEX, " ").trim();
List<String> parameters = buildParameterValues(configuration, boundSql);
return replacePlaceholders(sql, parameters);
}
/**
* 构建参数值集合。
*
* @param configuration MyBatis 配置
* @param boundSql 绑定 SQL
* @return 参数值集合
*/
private List<String> buildParameterValues(Configuration configuration, BoundSql boundSql) {
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
List<String> parameters = new ArrayList<>(parameterMappings.size());
Object parameterObject = boundSql.getParameterObject();
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
MetaObject metaObject = parameterObject == null ? null : configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() == ParameterMode.OUT) {
continue;
}
String propertyName = parameterMapping.getProperty();
Object value;
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else if (metaObject != null && metaObject.hasGetter(propertyName)) {
value = metaObject.getValue(propertyName);
} else {
value = null;
}
parameters.add(formatParameter(value));
}
return parameters;
}
/**
* 替换 SQL 占位符。
*
* @param sql SQL 模板
* @param parameters 参数集合
* @return 完整 SQL
*/
private String replacePlaceholders(String sql, List<String> parameters) {
if (parameters.isEmpty()) {
return sql;
}
StringBuilder builder = new StringBuilder(sql.length() + parameters.size() * 8);
int parameterIndex = 0;
boolean inSingleQuote = false;
boolean inDoubleQuote = false;
for (int i = 0; i < sql.length(); i++) {
char current = sql.charAt(i);
if (current == '\'' && !inDoubleQuote) {
inSingleQuote = !inSingleQuote;
} else if (current == '"' && !inSingleQuote) {
inDoubleQuote = !inDoubleQuote;
}
if (current == '?' && !inSingleQuote && !inDoubleQuote && parameterIndex < parameters.size()) {
builder.append(parameters.get(parameterIndex++));
} else {
builder.append(current);
}
}
return builder.toString();
}
/**
* 格式化参数值。
*
* @param value 参数值
* @return SQL 参数文本
*/
private String formatParameter(Object value) {
if (value == null) {
return "null";
}
if (value instanceof Number || value instanceof Boolean) {
return value.toString();
}
if (value instanceof Date date) {
return quote(DateUtil.formatDateTime(date));
}
if (value instanceof LocalDateTime localDateTime) {
return quote(localDateTime.format(DATE_TIME_FORMATTER));
}
if (value instanceof LocalDate localDate) {
return quote(localDate.format(DATE_FORMATTER));
}
if (value instanceof LocalTime localTime) {
return quote(localTime.format(TIME_FORMATTER));
}
if (value instanceof TemporalAccessor) {
return quote(value.toString());
}
if (value instanceof Enum<?> enumValue) {
return quote(enumValue.name());
}
return quote(value.toString());
}
/**
* 包装字符串参数。
*
* @param value 字符串值
* @return SQL 字符串参数
*/
private String quote(String value) {
return "'" + value.replace("'", "''") + "'";
}
/**
* 分片输出日志,避免日志链路截断超长 SQL。
*
* @param message 日志内容
*/
private void printChunk(String message) {
if (!isLogOutput()) {
printConsole(message);
return;
}
if (message.length() <= CHUNK_SIZE) {
print(message);
return;
}
String sqlLogId = UUID.randomUUID().toString();
int total = (message.length() + CHUNK_SIZE - 1) / CHUNK_SIZE;
for (int i = 0; i < total; i++) {
int start = i * CHUNK_SIZE;
int end = Math.min(start + CHUNK_SIZE, message.length());
print(StrUtil.format("sqlLogId={} part={}/{} {}", sqlLogId, i + 1, total, message.substring(start, end)));
}
}
/**
* 输出 SQL 日志。
*
* @param message 日志内容
*/
private void print(String message) {
if (isLogOutput()) {
log.info(message);
return;
}
printConsole(message);
}
/**
* 输出控制台日志。
*
* @param message 日志内容
*/
private void printConsole(String message) {
CONSOLE_LOCK.lock();
try {
System.err.println(message);
System.err.println();
} finally {
CONSOLE_LOCK.unlock();
}
}
/**
* 是否使用日志系统输出。
*
* @return true 使用日志系统输出,false 使用控制台输出
*/
private boolean isLogOutput() {
return StrUtil.equalsIgnoreCase("log", sqlLogProperties.getOutput());
}
}
@@ -0,0 +1,132 @@
package org.dromara.common.mybatis.utils;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.SpringUtils;
/**
* ID 生成工具类
*
* @author AprilWind
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class IdGeneratorUtil {
/**
* MyBatis-Plus 主键生成器
*/
private static final IdentifierGenerator GENERATOR = SpringUtils.getBean(IdentifierGenerator.class);
/**
* 生成字符串类型主键 ID
* <p>
* 调用 {@link IdentifierGenerator#nextId(Object)}返回 String 格式 ID
* </p>
*
* @return 字符串格式主键 ID
*/
public static String nextId() {
return GENERATOR.nextId(null).toString();
}
/**
* 生成 Long 类型主键 ID
* <p>
* 自动将生成的数字型主键转换为 Long 类型
* </p>
*
* @return Long 类型主键 ID
*/
public static Long nextLongId() {
return GENERATOR.nextId(null).longValue();
}
/**
* 生成 Number 类型主键 ID
* <p>
* 推荐在需要保留原始 Number 类型时使用
* </p>
*
* @return Number 类型主键 ID
*/
public static Number nextNumberId() {
return GENERATOR.nextId(null);
}
/**
* 根据实体生成数字型主键 ID
* <p>
* 若自定义的 {@link IdentifierGenerator} 根据实体内容生成 ID则可以使用本方法
* </p>
*
* @param entity 实体对象
* @return Number 类型主键 ID
*/
public static Number nextId(Object entity) {
return GENERATOR.nextId(entity);
}
/**
* 根据实体生成字符串主键 ID
* <p>
* {@link #nextId(Object)} 类似但返回 String 类型
* </p>
*
* @param entity 实体对象
* @return 字符串格式主键 ID
*/
public static String nextStringId(Object entity) {
return GENERATOR.nextId(entity).toString();
}
/**
* 生成 32 UUID
* <p>
* 底层使用 {@link IdWorker#get32UUID()}
* </p>
*
* @return 32 UUID 字符串
*/
public static String nextUUID() {
return IdWorker.get32UUID();
}
/**
* 根据实体生成 32 UUID
* <p>
* 默认 {@link IdentifierGenerator#nextUUID(Object)} 实现忽略实体但保留该方法便于扩展
* </p>
*
* @param entity 实体对象
* @return 32 UUID 字符串
*/
public static String nextUUID(Object entity) {
return GENERATOR.nextUUID(entity);
}
/**
* 生成带指定前缀的字符串主键 ID
* <p>
* 示例prefix = "ORD"生成结果形如{@code ORD20251211000123}
* </p>
*
* @param prefix 自定义前缀
* @return 带前缀的字符串主键 ID
*/
public static String nextIdWithPrefix(String prefix) {
return prefix + nextId();
}
/**
* 生成带前缀的 UUID
*
* @param prefix 前缀
* @return prefix + UUID
*/
public static String nextUUIDWithPrefix(String prefix) {
return prefix + nextUUID();
}
}
@@ -0,0 +1 @@
org.dromara.common.mybatis.config.MybatisPlusConfig
@@ -0,0 +1,31 @@
# 内置配置 不允许修改 如需修改请在 nacos 上写相同配置覆盖
# MyBatisPlus配置
# https://baomidou.com/config/
mybatis-plus:
# 启动时是否检查 MyBatis XML 文件的存在,默认不检查
checkConfigLocation: false
configuration:
# 自动驼峰命名规则(camel case)映射
mapUnderscoreToCamelCase: true
# MyBatis 自动映射策略
# NONE:不启用 PARTIAL:只对非嵌套 resultMap 自动映射 FULL:对所有 resultMap 自动映射
autoMappingBehavior: FULL
# MyBatis 自动映射时未知列或未知属性处理策
# NONE:不做处理 WARNING:打印相关警告 FAILING:抛出异常和详细信息
autoMappingUnknownColumnBehavior: NONE
# 关闭默认日志输出 org.apache.ibatis.logging.nologging.NoLoggingImpl
logImpl: org.apache.ibatis.logging.nologging.NoLoggingImpl
global-config:
# 是否打印 Logo banner
banner: true
dbConfig:
# 主键类型
# AUTO 自增 NONE 空 INPUT 用户输入 ASSIGN_ID 雪花 ASSIGN_UUID 唯一 UUID
idType: ASSIGN_ID
# 逻辑已删除值(可按需求随意修改)
logicDeleteValue: 1
# 逻辑未删除值
logicNotDeleteValue: 0
insertStrategy: NOT_NULL
updateStrategy: NOT_NULL
whereStrategy: NOT_NULL