需求描述
使用
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)
*/
@Test
public 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)
*/
@Test
public 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)
*/
@Test
public 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)
*/
@Test
public 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 执行结果
*/
@Override
public boolean removePhysicalById(Serializable id) {
return SqlHelper.retBool(baseMapper.deletePhysicalById(id));
}
...
/**
* 通过 query 条件,分页查询数据,返回指定类型
*
* @param query 查询条件
* @param clazz 需要转换的类型
* @return {@link PageInfo<S>}
*/
@Override
public <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
*/
@Override
public 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);
}
@Override
public String getMethod(SqlMethod sqlMethod) {
// 自定义 mapper 方法名
return "deletePhysicalById";
}
}
PhysicalDeleteEnhance
/**
* <p>
* 物理删除增强SQL注入器
* </p>
*
* @author walming
* @date 2021-03-09 10:19
*/
public class PhysicalDeleteEnhance extends DefaultSqlInjector {
@Override
public 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
*/
@Configuration
public class MybatisPlusConfig {
/**
* SQL 自动注入器接口
*/
@Bean
public 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:
SELECT
DATE_FORMAT( create_time, '%y-%m-%d' ) AS create_time,
COUNT(*) AS count
FROM
bg_visit_log
WHERE
( create_time >= '2021-07-05 00:00:00.0' AND create_time <= '2021-07-11 23:59:59.999' )
GROUP BY
DATE_FORMAT( create_time, '%y-%m-%d' )
ORDER BY
create_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
————————————————
参考项目:https://github.com/photowey/copycat