diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/query/QueryCriteria.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/query/QueryCriteria.java new file mode 100644 index 000000000..de21b248e --- /dev/null +++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/query/QueryCriteria.java @@ -0,0 +1,40 @@ +package org.dromara.common.mybatis.query; + +import java.lang.annotation.*; + +/** + * 查询条件 + * 修饰查询类qo + * + */ +@Target(value = {ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface QueryCriteria { + /** + * 条件, 例:QueryUtil.eq + */ + String criteria() default QueryUtil.eq; + + /** + * 字段, 默认为字段名 多表联查可通过 @QueryCriteria(name = "table.fieldName")配置具有别名的字段 + */ + String name() default ""; + + /** + * 跳过当前字段 + */ + boolean exit() default false; + + /** + * 是否必填 + */ + boolean required() default false; + + /** + * 排序方式 + * eq: QueryUtil.asc or QueryUtil.desc + */ + String order() default ""; + +} diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/query/QueryUtil.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/query/QueryUtil.java new file mode 100644 index 000000000..703dbd953 --- /dev/null +++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/query/QueryUtil.java @@ -0,0 +1,276 @@ +package org.dromara.common.mybatis.query; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.dromara.common.core.exception.ServiceException; +import org.springframework.util.ObjectUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.Optional; + +/** + * 参数处理 + * + * @author zjmm + */ +public class QueryUtil { + + /** + * 等价于 =,例: eq("name", "老王") ---> name = '老王' + */ + public final static String q_eq = "="; + + /** + * 等价于 <>,例: ne("name", "老王") ---> name <> '老王' + */ + public final static String q_ne = "<>"; + + /** + * 等价于 >,例: gt("name", "老王") ---> name > '老王' + */ + public final static String q_gt = ">"; + + /** + * 等价于 >=,例: ge("name", "老王") ---> name >= '老王' + */ + public final static String q_ge = ">="; + + /** + * 等价于 <,例: lt("name", "老王") ---> name < '老王' + */ + public final static String q_lt = "<"; + + /** + * 等价于 <=,例: le("name", "老王") ---> name <= '老王' + */ + public final static String q_le = "<="; + + /** + * 等价于 =,例: eq("name", "老王") ---> name = '老王' + */ + public final static String eq = "eq"; + + /** + * 等价于 <>,例: ne("name", "老王") ---> name <> '老王' + */ + public final static String ne = "ne"; + + /** + * 等价于 >,例: gt("name", "老王") ---> name > '老王' + */ + public final static String gt = "gt"; + + /** + * 等价于 >=,例: ge("name", "老王") ---> name >= '老王' + */ + public final static String ge = "ge"; + + /** + * 等价于 <,例: lt("name", "老王") ---> name < '老王' + */ + public final static String lt = "lt"; + + /** + * 等价于 <=,例: le("name", "老王") ---> name <= '老王' + */ + public final static String le = "le"; + + /** + * 等价于 between a and b, 例: between("age", 18, 30) ---> age between 18 and 30 + */ + public final static String between = "between"; + + /** + * 等价于 not between a and b, 例: notBetween("age", 18, 30) ---> age not between 18 and 30 + */ + public final static String notBetween = "notBetween"; + + /** + * 等价于 字段 IN (v0, v1, ...),例: in("age",{1,2,3}) ---> age in (1,2,3) + */ + public final static String in = "in"; + + /** + * 等价于 字段 NOT IN (v0, v1, ...), 例: notIn("age",{1,2,3}) ---> age not in (1,2,3) + */ + public final static String notIn = "notIn"; + + /** + * 等价于 字段 IN (sql 语句), 例: inSql("id", "select id from table where id < 3") ---> id in (select id from table where id < 3) + */ + public final static String inSql = "inSql"; + /** + * 等价于 字段 NOT IN (sql 语句) + */ + public final static String notInSql = "notInSql"; + + /** + * 等价于 LIKE '%值%',例: like("name", "王") ---> name like '%王%' + */ + public final static String like = "like"; + + /** + * 等价于 NOT LIKE '%值%',例: notLike("name", "王") ---> name not like '%王%' + */ + public final static String notLike = "notLike"; + + /** + * 等价于 LIKE '%值',例: likeLeft("name", "王") ---> name like '%王' + */ + public final static String likeLeft = "likeLeft"; + + /** + * 等价于 LIKE '值%',例: likeRight("name", "王") ---> name like '王%' + */ + public final static String likeRight = "likeRight"; + + /** + * 等价于 IS NULL,例: isNull("name") ---> name is null + */ + public final static String isNull = "isNull"; + + /** + * 等价于 IS NOT NULL,例: isNotNull("name") ---> name is not null + */ + public final static String isNotNull = "isNotNull"; + + public final static String asc = "asc"; + public final static String desc = "desc"; + + public static QueryWrapper toWrapper(QueryWrapper wrapper, M m) { + Class aClass = m.getClass(); + Field[] declaredFields = aClass.getDeclaredFields(); + + for (Field field : declaredFields) { + //判断field是否被static修饰 + if (Modifier.isStatic(field.getModifiers())) { + continue; + } + field.setAccessible(true); + //获取字段名 + QueryCriteria annotation = field.getAnnotation(QueryCriteria.class); + String name = StrUtil.toUnderlineCase(Optional.ofNullable(annotation) + .map(QueryCriteria::name) + .filter(StrUtil::isNotBlank) + .orElseGet(field::getName)); + + //获取排序方式 + String order = Optional.ofNullable(annotation).map(QueryCriteria::order).orElse(""); + + if (asc.equalsIgnoreCase(order)) { + wrapper.orderByAsc(name); + } + if (desc.equalsIgnoreCase(order)) { + wrapper.orderByDesc(name); + } + + if (Optional.ofNullable(annotation).map(QueryCriteria::exit).orElse(false)) { + continue; + } + + //获取比较方式 + String criteria = Optional.ofNullable(annotation).map(QueryCriteria::criteria).orElse(QueryUtil.eq); + + Object value = getValue(m, field); + if (ObjectUtils.isEmpty(value)) { + if (Optional.ofNullable(annotation).map(QueryCriteria::required).orElse(false)) { + throw new ServiceException(name + "参数内容必填"); + } else { + continue; + } + } + //添加条件 + addWrapper(wrapper, name, criteria, value); + } + + return wrapper; + } + + private static Object getValue(M m, Field field) { + try { + return field.get(m); + } catch (IllegalAccessException e) { + return null; + } + } + + /** + * 有查询条件调用 + * + * @param m qo对象 + * @return wrapper + */ + public static QueryWrapper toWrapper(M m) { + if (m == null) { + return new QueryWrapper(); + } + return toWrapper(new QueryWrapper(), m); + } + + /** + * 无查询条件调用 + */ + public static QueryWrapper newWrapper() { + return new QueryWrapper(); + } + + private static void addWrapper(QueryWrapper wrapper, String name, String criteria, Object value) { + switch (criteria) { + case q_eq, eq -> wrapper.eq(name, value); + case q_ne, ne -> wrapper.ne(name, value); + case q_gt, gt -> wrapper.gt(name, value); + case q_ge, ge -> wrapper.ge(name, value); + case q_lt, lt -> wrapper.lt(name, value); + case q_le, le -> wrapper.le(name, value); + case between -> { + if (value instanceof List list) { + if (list.size() >= 2) { + wrapper.between(name, list.get(0), list.get(1)); + break; + } + } else if (value instanceof Object[] arr) { + if (arr.length >= 2) { + wrapper.between(name, arr[0], arr[1]); + break; + } + } + throw new ServiceException(name + "字段传值请用list"); + } + case notBetween -> { + if (value instanceof List list) { + if (list.size() >= 2) { + wrapper.notBetween(name, list.get(0), list.get(1)); + break; + } + } else if (value instanceof Object[] arr) { + if (arr.length >= 2) { + wrapper.notBetween(name, arr[0], arr[1]); + break; + } + } + throw new ServiceException(name + "字段传值请用list"); + } + case in -> { + if (value instanceof List list) { + Object[] array = list.toArray(); + wrapper.in(name, array); + } else { + wrapper.in(name, value); + } + } + case notIn -> wrapper.notIn(name, value); + case inSql -> wrapper.inSql(name, value.toString()); + case notInSql -> wrapper.notInSql(name, value.toString()); + case like -> wrapper.like(name, value); + case notLike -> wrapper.notLike(name, value); + case likeLeft -> wrapper.likeLeft(name, value); + case likeRight -> wrapper.likeRight(name, value); + case isNull -> wrapper.isNull(name); + case isNotNull -> wrapper.isNotNull(name); + } + } + + +} diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/query/README.md b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/query/README.md new file mode 100644 index 000000000..a11ff37e7 --- /dev/null +++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/query/README.md @@ -0,0 +1,127 @@ +## 多条件多表查询功能优化 + +- 解决的问题 +在单表列表查询,多表联查列表查询,需要手动拼接条件,容易出错;这里引入工具类QueryUtil,通过Bo对象,自动拼接查询条件,减少手动拼接条件代码,提高开发效率。 + +```java +//项目内存在的代码片段 +public List selectDeptList(SysDeptBo dept) { + LambdaQueryWrapper lqw = buildQueryWrapper(dept); + return baseMapper.selectDeptList(lqw); +} +private LambdaQueryWrapper buildQueryWrapper(SysDeptBo bo) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(SysDept::getDelFlag, SystemConstants.NORMAL); + lqw.eq(ObjectUtil.isNotNull(bo.getDeptId()), SysDept::getDeptId, bo.getDeptId()); + lqw.eq(ObjectUtil.isNotNull(bo.getParentId()), SysDept::getParentId, bo.getParentId()); + lqw.like(StringUtils.isNotBlank(bo.getDeptName()), SysDept::getDeptName, bo.getDeptName()); + lqw.like(StringUtils.isNotBlank(bo.getDeptCategory()), SysDept::getDeptCategory, bo.getDeptCategory()); + lqw.eq(StringUtils.isNotBlank(bo.getStatus()), SysDept::getStatus, bo.getStatus()); + lqw.orderByAsc(SysDept::getAncestors); + lqw.orderByAsc(SysDept::getParentId); + lqw.orderByAsc(SysDept::getOrderNum); + lqw.orderByAsc(SysDept::getDeptId); + return lqw; +} +``` +以上面这段代码为例,查询部门列表,buildQueryWrapper方法中使用了eq,like, orderByAsc等wapper中的条件方法,直接返回wrapper,无需再手动添加条件。 +而在每个查询时都需要写这buildQueryWrapper方法,每次写条件拼接,易出错 + +这里改进为:扩展Bo, 为字段添加注解 +```java + @Data + public class SysDeptBo extends BaseEntity { + /** + * 祖级列表 + */ + @QueryCriteria(exit = true, order = "asc") + private String ancestors; + + /** + * 父部门ID + */ + @QueryCriteria(order = "asc") + private Long parentId; + /** + * 显示顺序 + */ + @NotNull(message = "显示顺序不能为空") + @QueryCriteria(exit = true, order = "asc") + private Integer orderNum; + + /** + * 部门id + */ + @QueryCriteria(order = "asc") + private Long deptId; + + /** + * 部门名称 + */ + @NotBlank(message = "部门名称不能为空") + @Size(min = 0, max = 30, message = "部门名称长度不能超过{max}个字符") + @QueryCriteria(criteria = QueryUtil.like) + private String deptName; + + /** + * 部门类别编码 + */ + @Size(min = 0, max = 100, message = "部门类别编码长度不能超过{max}个字符") + @QueryCriteria(criteria = QueryUtil.like) + private String deptCategory; + + /** + * 负责人 + */ + private Long leader; + + /** + * 联系电话 + */ + @Size(min = 0, max = 11, message = "联系电话长度不能超过{max}个字符") + private String phone; + + /** + * 邮箱 + */ + @Email(message = "邮箱格式不正确") + @Size(min = 0, max = 50, message = "邮箱长度不能超过{max}个字符") + private String email; + + /** + * 部门状态(0正常 1停用) + */ + private String status; + /** + * 删除标志(0代表存在 2代表删除) + */ + private String delFlag; + } + + public List selectDeptList(SysDeptBo dept) { + dept.setDelFlag(SystemConstants.NORMAL); + return baseMapper.selectDeptList(QueryUtil.toWrapper(dept)); + } + //为什么Bo中字段顺序改了,因为排序有前后顺序 + //为什么需要添加dept.setDelFlag(SystemConstants.NORMAL);这行代码,因为删除标志是默认的,在查询时,需要添加该条件,否则会查出所有数据 +``` +以上两种写法结果等价 +``` +使用前提: +1.domain添加qo,或者使用已有的Bo,甚至就使用domain, +2.使用@QueryCriteria注解修饰Qo中的实例字段,如果字段不配置注解,则使用默认配置 +3.使用mybatis-plus wrapper查询列表 +``` +``` +原理和使用介绍: +1、通过@QueryCriteria注解的参数criteria,可配置条件,包含多种sql查询类型,如eq,like,gt,lt,in,isNull,between等,详情查看QueryUtil工具类注释 +2、若字段不需要被加入sql条件中,注解参数exit设置为true +3、当qo中的实例字段的值为null时,该字段的查询条件将不被加入sql条件 +4、当表中的字段如dept_id,在一个接口中出现场景<1>筛选dept_id为null <2>筛选dept_id=xxx,可在qo中加入两个实例字段,名字自定义,注解参数name都设为dept_id,即可同时满足场景<1>和场景<2> +5、通过注解参数order,可实现字段排序 +6、当实例字段和注解参数name相同时,可省略注解参数name,多表联查可通过 @QueryCriteria(name = "table.fieldName")配置具有别名的字段 +7、or条件不支持,需自行拼接wapper +``` +### 根据自身的业务需求,可自定义规则,如: +- 前端不需要多余的参数,那么就可以扩展一个qo类,那么在前端的接口文档中就无需显示多余的参数,减少前后的沟通成本 +- QueryUtil.toWrapper(查询对象), 查询对象中的非static字段都会默认参与条件拼接,可以根据自己需要,考虑是否需要必须使用注解才参与