源码地址

需求描述

  • 使用 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.0mybatis-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