需求描述
使用
QueryWrapper对较少条件进行查询,如下:List<User> userList = userMapper.selectList(new QueryWrapper<User>().lambda().ge(User::getAge, 18));
而往往在真实情况下,前端一般有很多的查询条件传入后台,此时不得不手动的去包装我们的查询条件,如下:
List<User> userList = this.userService.list(new QueryWrapper<User>().lambda().ge(User::getAge, 18).likeLeft(User::getName, "ext").eq(User::getEmail, "ext@baomidou.com")...);
基础引用
mybatis-plus 3.4.0 与 mybatis-plus 3.4.3.4 SQL注入(DefaultSqlInjector)有所不同 ,可自行更正。
<!-- SpringBoot集成mybatis-plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.3.4</version></dependency>>
<!-- PageHelper-分页插件(非必须,可自行重写分页与排序模块) --><dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>1.3.0</version></dependency
实现原理
基于自定义注解通过反射来实现。
/*** 通过条件注解完成自动包装** @param query 自定义的查询对象* @param queryWrapper 查询包装器* @param <QUERY> 自定义的查询类型* @param <ENTITY> 实体类型* @return {@link QueryWrapper}*/public static <QUERY extends AbstractQuery, ENTITY> QueryWrapper<ENTITY> advise(final QUERY query, QueryWrapper<ENTITY> queryWrapper) {CriteriaFieldParser.foreachCriteriaField(query, (field, criteriaAnnotation) -> {final CriteriaAnnotationProcessor processorCached = findProcessor(criteriaAnnotation.annotationType());assert processorCached != null;return processorCached.process(queryWrapper, field, query, criteriaAnnotation);});return queryWrapper;}
查询使用
基本查询参数
/*** <p>* 基本查询参数* </p>** @author walming* @date 2021-03-09 14:03*/public class BaseQueryParam {/*** 基本查询参数数据库字段的命名策略(默认使用小写字母 && 下划线)* [orderByColumn,isAsc,selectColumn,excludeColumn]*/@ApiModelProperty(value = "数据库字段的命名策略", position = 100, example = "LOWER_CASE_UNDER_LINE")public ColumnNamingStrategy strategy = ColumnNamingStrategy.LOWER_CASE_UNDER_LINE;/** 排序列,不为空时执行排序,多个用逗号分隔 */@ApiModelProperty(value = "排序列,不为空时执行排序,多个用逗号分隔", example = "createTime", position = 101)private String orderByColumn;/** 排序方向(asc升序、desc倒序,多个用逗号分隔 */@ApiModelProperty(value = "排序方向(asc=升序,desc=倒序,多个用逗号分隔", example = "desc", position = 102)private String isAsc;/** 当前记录起始索引 */@ApiModelProperty(value = "当前记录起始索引", example = "1", position = 103)private Integer pageNum;/** 每页显示记录数(0表示查询出全部) */@ApiModelProperty(value = "每页显示记录数(0表示查询出全部)", example = "10", position = 104)private Integer pageSize;/** 指定需要查的询列(多个用逗号分隔,优先使用指定列) */@ApiModelProperty(value = "指定需要查询的列(多个用逗号分隔,优先使用指定列)", position = 105)private String selectColumn;/** 排除不需要查询的列(多个用逗号分隔,不能排除ID列) */@ApiModelProperty(value = "排除不需要查询的列(多个用逗号分隔,不能排除ID列)", position = 106)private String excludeColumn;public BaseQueryParam() {}public BaseQueryParam(Integer pageNum, Integer pageSize) {this.pageNum = pageNum;this.pageSize = pageSize;}public BaseQueryParam(String orderByColumn, String isAsc) {this.orderByColumn = orderByColumn;this.isAsc = isAsc;}public void setLimit(Integer pageNum, Integer pageSize) {this.pageNum = pageNum;this.pageSize = pageSize;}public void setOrderBy(String orderByColumn, String isAsc) {this.orderByColumn = orderByColumn;this.isAsc = isAsc;}.../** 获取排序SQL */public String getOrderBy() {return SQLUtils.getOrderBy(this, strategy);}}
自定义查询对象
继承内置的 AbstractQuery。
/*** <p>* 访问日志记录 bg_visit_log* </p>** @author walming* @date 2021-06-19 19:35:09*/@Data@Accessors(chain = true)@TableName(value = "bg_visit_log", excludeProperty = {"updateBy", "updateTime", "remark"})@EqualsAndHashCode(callSuper = false)@ApiModel("访问日志记录")public class VisitLog extends BaseEntity {/** 日志ID */@TableId(type = IdType.AUTO)@ApiModelProperty("日志ID")private String visitId;.../** 请求的模块 */@ApiModelProperty("请求的模块")private String title;/** 访问状态,1表示正常,0表示不正常 */@ApiModelProperty("访问状态,1表示正常,0表示不正常")private String status;}
/*** <p>* 访问日志记录查询参数* </p>** @author walming* @date 2021-06-19 19:35:09*/@Data@Accessors(chain = true)@EqualsAndHashCode(callSuper = false)@ApiModel("访问日志记录查询参数")public class VisitLogQuery extends AbstractQuery<VisitLog> {@Eq@ApiModelProperty(value = "日志ID", position = 1)private Integer visitId;@Like@ApiModelProperty(value = "请求的模块", position = 2)private String title;@Eq@ApiModelProperty(value = "访问状态,1表示正常,0表示不正常", position = 3)private String status;@GroupBy(fieldType = HandleTypeEnum.DYNAMIC, naming = ColumnNamingStrategy.DEFAULT_COLUMN_NAMING)@ApiModelProperty(value = "分组统计(需配合selectColumn一起使用)", position = 4)private String groupBy;@JsonFormat(pattern = "yyyy-MM-dd")@Time(alias = "create_time", compare = CompareEnum.GE, format = TimeQueryFormat.DAY_BEGIN)@ApiModelProperty(value = "开始时间段", position = 10)private Date beginTime;@JsonFormat(pattern = "yyyy-MM-dd")@Time(alias = "create_time", compare = CompareEnum.LE, format = TimeQueryFormat.DAY_END)@ApiModelProperty(value = "结束时间段", position = 11)private Date endTime;}
使用案列
@Eq
/*** 等于(=) 条件注解** @author walming* @date 2020-02-15 19:45*/@Documented@CriteriaQuery@Target({ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)public @interface Eq {/** 自定义的属性值(数据库字段名) */String alias() default "";/** 数据库字段命名策略(默认小写字母 && 下划线) */ColumnNamingStrategy naming() default ColumnNamingStrategy.LOWER_CASE_UNDER_LINE;}
测试代码:
VisitLogQuery visitLogQuery = new VisitLogQuery().setVisitId(1);VisitLog visitLog = visitLogService.getOne(visitLogQuery.autoWrapper());
最终SQL:
SELECT visit_id,ip_addr,location,browser,os,spider,request_url,error_msg,title,status,create_by,create_time FROM bg_visit_log WHERE (visit_id = 1)
@Like
/*** 模糊查询(LIKE) 条件注解** @author walming* @date 2020-02-15 19:50*/@Documented@CriteriaQuery@Target({ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)public @interface Like {/** 自定义的属性值(数据库字段名) */String alias() default "";/** 匹配模式 */SqlLike like() default SqlLike.DEFAULT;/** 数据库字段命名策略(默认小写字母 && 下划线) */ColumnNamingStrategy naming() default ColumnNamingStrategy.LOWER_CASE_UNDER_LINE;}
public enum SqlLike {LEFT,RIGHT,DEFAULT;...}
测试代码:
VisitLogQuery visitLogQuery = new VisitLogQuery();visitLogQuery.setTitle("首页");visitLogQuery.setOrderBy("status,create_time", "asc,desc");List<VisitLog> logList = visitLogService.list(visitLogQuery.autoWrapper());
最终SQL:
SELECT visit_id, ip_addr, location, browser, os, spider, request_url, error_msg, title, status, create_by, create_time FROM bg_visit_log WHERE (title LIKE '%首页%') order by status asc,create_time desc
@Time
/*** <p>* 时间模式查询,根据不同场景对时间进行处理* </p>** @author walming* @date 2021-04-14 11:04*/@Documented@CriteriaQuery@Target({ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)public @interface Time {/** 自定义的属性值(数据库字段名) */String alias() default "";/** 比较运行符,默认采用等于 */CompareEnum compare() default CompareEnum.EQ;/** 数据库字段命名策略(默认小写字母 && 下划线) */ColumnNamingStrategy naming() default ColumnNamingStrategy.LOWER_CASE_UNDER_LINE;/** 时间查询模式,默认不做任何处理 */TimeQueryFormat format() default TimeQueryFormat.DEFAULT;}
/*** <p>* 时间查询模式* </p>** @author walming* @date 2021-04-14 9:39*/public enum TimeQueryFormat {/** 默认模式 */DEFAULT("yyyy-MM-dd HH:mm:ss.SSS"),/** 精确到小时 */HOUR("yyyy-MM-dd HH:00:00.0"),/** 精确到分钟 */MINUTE("yyyy-MM-dd HH:mm:00.0"),/** 精确到秒 */SECOND("yyyy-MM-dd HH:mm:ss.0"),/** 表示一天的开始 */DAY_BEGIN("yyyy-MM-dd 00:00:00.0"),/** 表示一天的结束 */DAY_END("yyyy-MM-dd 23:59:59.999");private final String format;TimeQueryFormat(String format) {this.format = format;}public String format() {return format;}}
测试代码:
// 获取 2021-07-01 至 2021-07-05 期间的数据,包括 2021-07-05 这一天的数据VisitLogQuery visitLogQuery = new VisitLogQuery();visitLogQuery.setTitle("首页");visitLogQuery.setBeginTime(DateUtils.parseDate("2021-07-05"));visitLogQuery.setEndTime(DateUtils.parseDate("2021-07-11"));visitLogQuery.setOrderBy("status,create_time", "asc,desc");List<VisitLog> logList = visitLogService.list(visitLogQuery.autoWrapper());
最终SQL:
SELECT visit_id, ip_addr, location, browser, os, spider, request_url, error_msg, title, status, create_by, create_time FROM bg_visit_log WHERE (title LIKE '%首页%' AND create_time >= '2021-07-05 00:00:00.0' AND create_time <= '2021-07-11 23:59:59.999') order by status asc,create_time desc
@Timestamp
一些场景下,前端时间传值,可能是采用的是时间戳 - 因此就有了 @Timestamp 的使用场景。
/*** 测试 Timestamp* {@link java.time.LocalDateTime}* ==> Preparing: SELECT id,name,age,email,birth_day FROM user WHERE (age >= ? AND birth_day > ?) ORDER BY age DESC* ==> Parameters: 20(Integer), 2019-05-11T10:00(LocalDateTime)*/@Testpublic void testQueryByTimestampGe() throws ParseException {String birthDayStr = "2019-05-12 10:00:00";SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");Date birthDay = format.parse(birthDayStr);UserQuery userQuery = new UserQuery().setAge(20).setBirthDayGe(birthDay);List<User> users = this.userService.list(userQuery.autoWrapper());Assert.assertEquals(3, users.size());users.forEach(System.out::println);}/*** 测试 Timestamp* {@link java.time.LocalDate}* ==> Preparing: SELECT id,name,age,email,birth_day FROM user WHERE (age >= ? AND birth_day > ?) ORDER BY age DESC* ==> Parameters: 20(Integer), 2019-05-11(LocalDate)*/@Testpublic void testQueryByCriteriaTimestampByLocalDate() {UserQuery userQuery = new UserQuery().setAge(20).setBirthDayTimestampLocalDate(1557540000000L);List<User> users = this.userService.list(userQuery.autoWrapper());users.forEach(System.out::println);}/*** 测试 Timestamp* {@link java.time.LocalTime}* ==> Preparing: SELECT id,name,age,email,birth_day FROM user WHERE (age >= ? AND birth_day > ?) ORDER BY age DESC* ==> Parameters: 20(Integer), 10:00(LocalTime)*/@Testpublic void testQueryByCriteriaTimestampByLocalTime() {UserQuery userQuery = new UserQuery().setAge(20).setBirthDayTimestampLocalTime(1557540000000L);List<User> users = this.userService.list(userQuery.autoWrapper());users.forEach(System.out::println);}/*** 测试 Timestamp* {@link java.time.ZonedDateTime}* ==> Preparing: SELECT id,name,age,email,birth_day FROM user WHERE (age >= ? AND birth_day > ?) ORDER BY age DESC* ==> Parameters: 20(Integer), 2019-05-11T10:00+08:00[Asia/Shanghai](ZonedDateTime)*/@Testpublic void testQueryByCriteriaTimestampByZonedDateTime() {UserQuery userQuery = new UserQuery().setAge(20).setBirthDayTimestampZonedDateTime(1557540000000L);List<User> users = this.userService.list(userQuery.autoWrapper());users.forEach(System.out::println);}
传参排序处理规则
/*** 针对多字段排序规则优化** @param queryParam 基本查询参数* @param strategy 数据库字段的命名策略* @return 排序条件语句*/public static String getOrderBy(BaseQueryParam queryParam, ColumnNamingStrategy strategy) {// 排序字段为空返回空字符串if (StringUtils.isEmpty(queryParam.getOrderByColumn())) {return StringConstants.EMPTY_STRING;}String orderByColumn = StringUtils.replaceAllBlank(queryParam.getOrderByColumn());// 获取需要排序的字段String[] columns = Convert.toStrArray(NamingUtils.columnName(orderByColumn, strategy));// 若排序方向只有一个,默认所有字段均以该方向进行排序StringBuilder orderingRule = new StringBuilder();// 当排序方向为空时不做处理if (StringUtils.isEmpty(queryParam.getIsAsc())) {orderingRule.append(StringUtils.join(columns, StringConstants.COMMA));} else {String isAsc = StringUtils.replaceAllBlank(queryParam.getIsAsc());String[] directions = Convert.toStrArray(isAsc);// 当排序方向只有一个时默认所有字段均以此方向排序if (directions.length == 1) {for (int i = 0; i < columns.length; i++) {orderingRule.append(columns[i]).append(StringConstants.BLANK_SPACE).append(getIsAsc(directions[0]));if (i < columns.length - 1) {orderingRule.append(StringConstants.COMMA);}}} else {// 当排序方向有多个时按顺序匹配,若有多出或不足将不做处理for (int i = 0; i < columns.length; i++) {orderingRule.append(columns[i]);if (directions.length >= i) {orderingRule.append(StringConstants.BLANK_SPACE).append(getIsAsc(directions[i]));}if (i < columns.length - 1) {orderingRule.append(StringConstants.COMMA);}}}}return orderingRule.toString();}/** 排序方向规范,不合理默认升序 */public static String getIsAsc(String isAsc) {return (QueryConstants.ASC.equals(isAsc) || QueryConstants.DESC.equals(isAsc)) ? isAsc : QueryConstants.ASC;}
自定义模版方法
通过继承与重写 mybatis-plus 提供的 IService、ServiceImpl可以新增一些自己的模板方法。
通过mybatis-plus提供的SQL注入器(DefaultSqlInjector)也可以新增一些自己的 BaseMapper 查询模板。
IService
/*** <p>* 自定义IService* </p>** @author walming* @date 2021-03-09 09:53*/public interface IService<T> extends com.baomidou.mybatisplus.extension.service.IService<T> {.../*** 根据 ID 删除(物理删除,不受逻辑标识影响)** @param id 主键ID* @return 执行结果*/boolean removePhysicalById(Serializable id);.../*** 通过 query 条件,分页查询数据,并进行排序,返回指定类型** @param query 查询条件* @param clazz 需要转换的类型* @return {@link PageInfo<T>}*/<S> PageInfo<S> query(AbstractQuery<T> query, Class<S> clazz);}
ServiceImpl
/*** <p>* 自定义ServiceImpl* </p>** @author walming* @date 2021-03-09 09:55*/public class ServiceImpl<M extends BaseMapper<T>, T> extends com.baomidou.mybatisplus.extension.service.impl.ServiceImpl<M, T> implements IService<T> {.../*** 根据 ID 删除(物理删除,不受逻辑标识影响)** @param id 主键ID* @return 执行结果*/@Overridepublic boolean removePhysicalById(Serializable id) {return SqlHelper.retBool(baseMapper.deletePhysicalById(id));}.../*** 通过 query 条件,分页查询数据,返回指定类型** @param query 查询条件* @param clazz 需要转换的类型* @return {@link PageInfo<S>}*/@Overridepublic <S> PageInfo<S> query(AbstractQuery<T> query, Class<S> clazz) {List<Map<String, Object>> maps = baseMapper.selectMaps(query.autoWrapper());List<S> list = BeanUtils.mapsToBeanList(maps, clazz);return BeanUtils.copyToCast(new PageInfo<>(maps), new PageInfo<>(list), "list");}}
BaseMapper
/*** BaseMapper** @author walming* @date 2021-03-09 9:52*/public interface BaseMapper<T> extends com.baomidou.mybatisplus.core.mapper.BaseMapper<T> {.../*** 根据 ID 删除(物理删除,不受逻辑标识影响)** @param id 主键ID* @return 执行影响行数*/int deletePhysicalById(Serializable id);...}
PhysicalDeleteById
/*** <p>* 根据 ID 删除数据,<br>* 此方法慎用,绝对物理删除,不受逻辑标识影响* </p>** @author walming* @date 2021-03-09 11:45*/public class PhysicalDeleteById extends AbstractMethod {/*** 注入自定义 MappedStatement** @param mapperClass mapper 接口* @param modelClass mapper 泛型* @param tableInfo 数据库表反射信息* @return MappedStatement*/@Overridepublic MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {SqlMethod sqlMethod = SqlMethod.DELETE_BY_ID;String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), tableInfo.getKeyColumn(), tableInfo.getKeyProperty());SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Object.class);return this.addDeleteMappedStatement(mapperClass, getMethod(sqlMethod), sqlSource);}@Overridepublic String getMethod(SqlMethod sqlMethod) {// 自定义 mapper 方法名return "deletePhysicalById";}}
PhysicalDeleteEnhance
/*** <p>* 物理删除增强SQL注入器* </p>** @author walming* @date 2021-03-09 10:19*/public class PhysicalDeleteEnhance extends DefaultSqlInjector {@Overridepublic List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo);methodList.add(new PhysicalDelete());methodList.add(new PhysicalDeleteById());methodList.add(new PhysicalDeleteBatchByIds());return methodList;}}
MybatisPlusConfig
/*** mybatis-plus 配置类** @author walming* @date 2021-03-03 16:07*/@Configurationpublic class MybatisPlusConfig {/*** SQL 自动注入器接口*/@Beanpublic ISqlInjector sqlInjector() {return new PhysicalDeleteEnhance();}}
进阶使用
分页查询
测试代码:
VisitLogQuery visitLogQuery = new VisitLogQuery();visitLogQuery.setLimit(1, 10);PageInfo<VisitLog> pageInfo = visitLogService.query(visitLogQuery);
最终SQL:
SELECT visit_id,ip_addr,location,browser,os,spider,request_url,error_msg,title,status,create_by,create_time FROM bg_visit_log LIMIT 10
自定义返回列&对象
@Data@Accessors(chain = true)public class VisitLogTest {/** 浏览器类型 */private String visitId;/** 操作系统 */private String os;/** 请求地址 */private String requestUrl;/** 请求的模块 */private String module;}
测试代码:
VisitLogQuery visitLogQuery = new VisitLogQuery();visitLogQuery.setSelectColumn("visitId, os, requestUrl, title as module, spider");visitLogQuery.setOrderBy("create_time", "desc");visitLogQuery.setLimit(1, 10);PageInfo<VisitLogTest> pageInfo = visitLogService.query(visitLogQuery, VisitLogTest.class);
最终SQL:
SELECT visit_id, os, request_url, title AS module, spider FROM bg_visit_log order by create_time desc LIMIT 10
分组统计
@Data@Accessors(chain = true)public class DateGroupingStatistics {/** 时间(天) */private String createTime;/** 识别数 */private Integer count;}
测试代码:
// 统计 2021-07-05 至 2021-07-11 这一周每天的日志数量VisitLogQuery visitLogQuery = new VisitLogQuery();visitLogQuery.setStrategy(ColumnNamingStrategy.DEFAULT_COLUMN_NAMING);visitLogQuery.setBeginTime(DateUtils.parseDate("2021-07-05"));visitLogQuery.setEndTime(DateUtils.parseDate("2021-07-11"));visitLogQuery.setSelectColumn("DATE_FORMAT(create_time, '%y-%m-%d') AS create_time, COUNT(*) AS count");visitLogQuery.setGroupBy("DATE_FORMAT(create_time, '%y-%m-%d')");visitLogQuery.setOrderByColumn("create_time");List<DateGroupingStatistics> logStatistics = visitLogService.list(visitLogQuery, DateGroupingStatistics.class);
最终SQL:
SELECTDATE_FORMAT( create_time, '%y-%m-%d' ) AS create_time,COUNT(*) AS countFROMbg_visit_logWHERE( create_time >= '2021-07-05 00:00:00.0' AND create_time <= '2021-07-11 23:59:59.999' )GROUP BYDATE_FORMAT( create_time, '%y-%m-%d' )ORDER BYcreate_time
打印日志:
==> Parameters:<== Columns: create_time, count<== Row: 21-07-07, 15<== Row: 21-07-08, 30<== Row: 21-07-09, 18<== Row: 21-07-10, 12<== Row: 21-07-11, 20<== Total: 5
