# mybatis-plus

# MyBatis-Plus BaseMapper 是怎样实现的

# 批量插入saveBatch

# 扩充默认sql

# mybatisplus 的常用 CRUD 方法

众所周知,mybatisplus 提供了强大的代码生成能力,他默认生成的常用的 CRUD 方法(例如插入更新删除查询等)的定义,能够帮助我们节省很多体力劳动。

他的 BaseMapper 中定义了这些常用的 CRUD 方法,我们在使用时,继承这个 BaseMapper 类就默认拥有了这些能力。

如果我们的业务中,需要类似的通用 Sql 时,该如何实现呢?

是每个 Mapper 中都定义一遍类似的 sql 吗?

显然这是最笨的一种方法。

此时我们可以借助 mybatisplus 这个成熟框架,来实现我们想要的通用 Sql。

# 扩展常用 CRUD 方法

# 新增一个通用 sql

比如有一个这样的需求,项目中所有表或某一些表,都要执行一个类似的查询,如 SelectByErp,那么可以这样实现。(这是一个最简单的 sql 实现,使用时可以根据业务需求实现更为复杂的 sql:比如多租户系统自动增加租户 id 参数、分库分表系统增加分库分表字段条件判断)
定义一个 SelectByErp 类,继承 AbstractMethod 类,并实现 injectMappedStatement 方法 定义 sql 方法名、sql 模板、实现 sql 的拼接组装

/**
 * 新增一个通用sql
 */
public class SelectByErp extends AbstractMethod {
     // 需要查询的列名
    private final String erpColumn = "erp";
    // sql方法名
    private final String method = "selectByErp";
    // sql模板
    private final String sqlTemplate = "SELECT %s FROM %s WHERE %s=#{%s} %s";

    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
       	// 获取需要查询的字段名及属性名
        TableFieldInfo erpFiled = getErpProperty(tableInfo);
        // 拼接组装sql
        SqlSource sqlSource = new RawSqlSource(configuration, String.format(sqlTemplate,
                sqlSelectColumns(tableInfo, false),
                tableInfo.getTableName(), 
                erpFiled.getColumn(), erpFiled.getProperty(),
                tableInfo.getLogicDeleteSql(true, false)), Object.class);
        return this.addSelectMappedStatementForTable(mapperClass, method, sqlSource, tableInfo);
}
	/**
     * 查询erp列信息
     */
    private TableFieldInfo getErpProperty(TableInfo tableInfo) {
        List<TableFieldInfo> fieldList = tableInfo.getFieldList();
        TableFieldInfo erpField = fieldList.stream().filter(filed -> filed.getColumn().equals(erpColumn)).findFirst().get();
        return erpField;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 定义一个 sql 注入器 GyhSqlInjector,添加 SelectByErp 对象

// 需注入到spring容器中
@Component
public class GyhSqlInjector extends DefaultSqlInjector {    
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        List<AbstractMethod> methodList = super.getMethodList(mapperClass);
        // 增加 SelectByErp对象,程序启动后自动加载
        methodList.add(new SelectByErp());
        return methodList;
    }
}
1
2
3
4
5
6
7
8
9
10
11
  1. 定义一个基础 MapperGyhBaseMapper,添加 selectByErp 方法
/**
 * 自定义的通用Mapper
 */
public interface GyhBaseMapper<T> extends BaseMapper<T> {
    List<T> selectByErp(String erp);
}
1
2
3
4
5
6
  1. 应用中需要使用该 SelectByErp 方法的表,都继承 GyhBaseMapper,那么这些表将都拥有了 selectByErp 这个查询方法,程序启动后会自动为这些表生成该 sql。

public interface XXXMapper extends GyhBaseMapper 添加一个 mybatisplus 已有 sql 1.mybatisplus 常用 CRUD 方法如最上图,这些方法已经默认会自动生成,但 mybatisplus 其实提供了更多的方法,如下图,只要我们在启动时添加进去,就可以使用了。

  1. 比如我想使用 AlwaysUpdateSomeColumnById 方法,该方法可以在更新时只更新我需要的字段,不进行全字段更新。添加步骤如下。

  2. 定义一个 sql 注入器 ,如 GyhSqlInjector,添加 AlwaysUpdateSomeColumnById 对象

@Component
public class GyhSqlInjector extends DefaultSqlInjector {    
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        List<AbstractMethod> methodList = super.getMethodList(mapperClass);
        // 添加 AlwaysUpdateSomeColumnById 对象
        methodList.add(new AlwaysUpdateSomeColumnById());
        return methodList;
    }
}
1
2
3
4
5
6
7
8
9
10
  1. 定义一个基础 Mapper 如 GyhBaseMapper,添加 alwaysUpdateSomeColumnById 方法
/**
 * 自定义的通用Mapper
 */
public interface GyhBaseMapper<T> extends BaseMapper<T> {
    int alwaysUpdateSomeColumnById(@Param(Constants.ENTITY) T entity);
}
1
2
3
4
5
6
  1. 继承 GyhBaseMapper 的其他 Mapper,将自动拥有 alwaysUpdateSomeColumnById 方法
/**
 * 自定义的通用Mapper
 */
public interface GyhBaseMapper<T> extends BaseMapper<T> {
    int alwaysUpdateSomeColumnById(@Param(Constants.ENTITY) T entity);
}
1
2
3
4
5
6
  1. 继承 GyhBaseMapper 的其他 Mapper,将自动拥有 alwaysUpdateSomeColumnById 方法

编辑一个 mybatisplus 已有 sql

  1. 如果想编辑一个 mybatisplus 已有 sql,比如分库分表系统,执行 updateById 操作时,虽然主键 Id 已确定,但目标表不确定,此时可能导致该 sql 在多张表上执行,造成资源浪费,并且分库分表字段不可修改,默认的 updateById 不能用,需要改造。以下以 Shardingsphere 分库分表为例。

  2. 定义一个 UpdateByIdWithSharding 类,继承 UpdateById 类

public class UpdateByIdWithSharding extends UpdateById {
    private String columnDot = "`";
    private YamlShardingRuleConfiguration yamlShardingRuleConfiguration;
    // 注入shardingsphere的分库分表配置信息
    public UpdateByIdWithSharding(yamlShardingRuleConfiguration yamlShardingRuleConfiguration) {
        this.yamlShardingRuleConfiguration = yamlShardingRuleConfiguration;
    }

    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        String tableName = tableInfo.getTableName();
        // shardingsphere 分库分表配置信息
        Map<String, YamlTableRuleConfiguration> tables = yamlShardingRuleConfiguration.getTables();
        // 判断当前表是否设置了分表字段
        if (tables.containsKey(tableName)) {
            YamlTableRuleConfiguration tableRuleConfiguration = tables.get(tableName);
            // 获取分表字段
            String shardingColumn = tableRuleConfiguration.getTableStrategy().getStandard().getShardingColumn();
            // 构建sql
            boolean logicDelete = tableInfo.isLogicDelete();
            SqlMethod sqlMethod = SqlMethod.UPDATE_BY_ID;
            // 增加分表字段判断
            String shardingAdditional = getShardingColumnWhere(tableInfo, shardingColumn);
            // 是否判断逻辑删除字段
            final String additional = optlockVersion() + tableInfo.getLogicDeleteSql(true, false);
            shardingAdditional = shardingAdditional + additional;
            String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(),
                    getSqlSet(logicDelete, tableInfo, shardingColumn),
                    tableInfo.getKeyColumn(), ENTITY_DOT + tableInfo.getKeyProperty(),
                    shardingAdditional);
            SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
            return addUpdateMappedStatement(mapperClass, modelClass, sqlMethod.getMethod(), sqlSource);
        } else {
            return super.injectMappedStatement(mapperClass, modelClass, tableInfo);
        }
    }

    /**
     * where条件增加分表字段
     */
    private String getShardingColumnWhere(TableInfo tableInfo, String shardingColumn) {
        StringBuilder shardingWhere = new StringBuilder();
        shardingWhere.append(" AND ").append(shardingColumn).append("=#{");
        shardingWhere.append(ENTITY_DOT);
        TableFieldInfo fieldInfo = tableInfo.getFieldList().stream()
                .filter(f -> f.getColumn().replaceAll(columnDot, StringUtils.EMPTY).equals(shardingColumn))
                .findFirst().get();
        shardingWhere.append(fieldInfo.getEl());
        shardingWhere.append("}");
        return shardingWhere.toString();
    }

    /**
     * set模块去掉分表字段
     */
    public String getSqlSet(boolean ignoreLogicDelFiled, TableInfo tableInfo, String shardingColumn) {
        List<TableFieldInfo> fieldList = tableInfo.getFieldList();
        // 去掉分表字段的set设置,即不修改分表字段
        String rmShardingColumnSet = fieldList.stream()
                .filter(i -> ignoreLogicDelFiled ? !(tableInfo.isLogicDelete() && i.isLogicDelete()) : true)
                .filter(i -> !i.getColumn().equals(shardingColumn))
                .map(i -> i.getSqlSet(ENTITY_DOT))
                .filter(Objects::nonNull).collect(joining(NEWLINE));
        return rmShardingColumnSet;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
  1. 定义一个 sql 注入器 GyhSqlInjector,添加 UpdateByIdWithSharding 对象
// 需注入到spring容器中
@Component
public class GyhSqlInjector extends DefaultSqlInjector {    
    /**
     * shardingsphere 配置信息
     */
    @Autowired
    private YamlShardingRuleConfiguration yamlShardingRuleConfiguration;

    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        List<AbstractMethod> methodList = super.getMethodList(mapperClass);
        // 添加 UpdateByIdWithSharding 对象,并注入分库分表信息
        methodList.add(new UpdateByIdWithSharding(yamlShardingRuleConfiguration));
        return methodList;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  1. 定义一个基础 MapperGyhBaseMapper,添加新的 selectById 方法
/**
 * 自定义的通用Mapper
 */
public interface GyhBaseMapper<T> extends BaseMapper<T> {
   int updateById(@Param(Constants.ENTITY) T entity);
}
1
2
3
4
5
6
  1. 所有参与分表的表,在定义 Mapper 时继承 GyhBaseMapper,那么在使用他的 updateById 方法时,将自动增加分库分表判断,准确命中目标表,减少其他分表查询的资源浪费。

# 打印sql日志

  • 1、设置mybatis-plus包下的日志级别
  • 2、设置项目mapper目录的日志级别
  • 3、设置mybatis-plus的日志输出方式为slf4j
logging.level.com.baomidou.mybaitsplus=DEBUG
logging.level.com.ccbft.atf.agile.mapper=DEBUG
mybaits-plus.configuration.log-impl=org.apache.ibatis.logging.slf4j.Slf4jImpl
1
2
3

# MyBatis-plus最详细的入门

# 1.概述

MyBatis-Plus (简称 MP,下文就使用简称啦)是一个 MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。官网地址:https://baomidou.com/ 有以下特性:

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  • 内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

接下来本文会围绕Spring Boot整合Mybatis-plus,从引入、配置、使用等几个方面进行总结汇总,足以覆盖我们日常开发中绝大部分使用场景

# 2.快速开始

使用IDEA创建一个Spring Boot项目,添加如下依赖:

        <!-- MySQL连接驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.19</version>
        </dependency>

        <!-- mp依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13

配置文件添加数据源:

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql://127.0.0.1:3306/db_test?&serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF8&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true
1
2
3
4
5
6

# 3.使用示例

首先先在数据库添加一个用户表:tb_user

CREATE TABLE `tb_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_no` varchar(255) NOT NULL COMMENT '编号',
  `nickname` varchar(255) DEFAULT NULL COMMENT '昵称',
  `email` varchar(255) DEFAULT NULL COMMENT '邮箱',
  `phone` varchar(255) NOT NULL COMMENT '手机号',
  `gender` tinyint(4) NOT NULL DEFAULT '0' COMMENT '性别  0:男生   1:女生',
  `birthday` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '出生日期',
  `is_delete` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标志 0:否  1:是',
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
1
2
3
4
5
6
7
8
9
10
11
12
13

在项目服务中添加对应的实体类:User

@Data
@TableName(value = "tb_user")
public class User {

    @TableId(type = IdType.AUTO)
    private Long id;
    private String userNo;
    private String nickname;
    private String email;
    private String phone;
    private Integer gender;
    private Date birthday;
    private Integer isDelete;
    private Date createTime;
    private Date updateTime;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

创建对应的mapper接口:UserDAO,这里我以DAO结尾,平时习惯使用这个了

public interface UserDAO extends BaseMapper<User> {

}
1
2
3

最后启动类添加配置扫描:

@SpringBootApplication
@MapperScan(basePackages = "com.shepherd.mybatisplus.demo.dao")
public class MybatisPlusDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(MybatisPlusDemoApplication.class, args);
    }

}
1
2
3
4
5
6
7
8
9

添加测试类:

@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTest {
    @Resource
    private UserService userService;

    @Resource
    private UserDAO userDAO;

    /**
     * 添加一条记录
     */
    @Test
    public void testAdd() {
        User user = User.builder()
                .id(1L)
                .userNo("001")
                .nickname("大哥")
                .email("shepherd@qq.com")
                .phone("1234556")
                .birthday(new Date())
                .build();
        userDAO.insert(user);
    }


    /**
     * 查询所有记录
     */
    @Test
    public void testList() {
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        List<User> users = userDAO.selectList(queryWrapper);
        System.out.println(users);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

执行testAdd插入方法之后,再执行testList,控制台输出如下:

2023-11-08 16:50:58.514  INFO 12636 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2023-11-08 16:50:58.720  INFO 12636 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2023-11-08 16:50:58.734 DEBUG 12636 --- [           main] c.s.m.demo.dao.UserDAO.selectList        : ==>  Preparing: SELECT id,user_no,nickname,email,phone,gender,birthday,is_delete,create_time,update_time FROM tb_user
2023-11-08 16:50:58.763 DEBUG 12636 --- [           main] c.s.m.demo.dao.UserDAO.selectList        : ==> Parameters: 
2023-11-08 16:50:58.792 DEBUG 12636 --- [           main] c.s.m.demo.dao.UserDAO.selectList        : <==      Total: 1
[User(id=1, userNo=001, nickname=大哥, email=shepherd@qq.com, phone=1234556, gender=0, birthday=Wed Nov 08 16:26:58 CST 2023, isDelete=0, createTime=Wed Nov 08 16:26:58 CST 2023, updateTime=Wed Nov 08 16:26:58 CST 2023)]
1
2
3
4
5
6

上面的示例就是我们平时开发过程单表的基本CRUD操作,只需要创建好实体类,并创建一个继承自BaseMapper的接口即可,是不是很简单,不需要我们有过多的编码和配置操作啥的。因为mp会自动做了数据库表名、字段名映射,Java类的驼峰命名和下划线字段之间的转化

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

# 3.注解

mp封装了很多注解供我们使用,下面我们就可以来看看常用的注解:

# @TableName

表名注解,标识实体类对应的表,实体类的类名(转成小写后)和数据库表名相同时,可以不指定该注解。定义如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
public @interface TableName {

    /**
     * 实体对应的表名,实体类的类名(转成小写后)和数据库表名相同时,可以不指定
     */
    String value() default "";

    /**
     * schema
    */
    String schema() default "";

    /**
     * 是否保持使用全局的 tablePrefix 的值,只生效于 既设置了全局的 tablePrefix 也设置了上面 {@link #value()} 的值
     * @since 3.1.1
     */
    boolean keepGlobalPrefix() default false;

    /**
     * 实体映射结果集,
     * 只生效与 mp 自动注入的 method
     */
    String resultMap() default "";

    /**
     * 是否自动构建 resultMap 并使用,
     * 只生效与 mp 自动注入的 method,
     * 如果设置 resultMap 则不会进行 resultMap 的自动构建并注入,
     * 只适合个别字段 设置了 typeHandler 或 jdbcType 的情况
     *
     * @since 3.1.2
     */
    boolean autoResultMap() default false;

    /**
     * 需要排除的属性名
     */
    String[] excludeProperty() default {};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

这里对 autoResultMap 这个属性做如下说明强调:

MP 会自动构建一个 resultMap 并注入到 MyBatis 里(一般用不上),请注意以下内容:

因为 MP 底层是 MyBatis,所以 MP 只是帮您注入了常用 CRUD 到 MyBatis 里,注入之前是动态的(根据您的 Entity 字段以及注解变化而变化),但是注入之后是静态的(等于 XML 配置中的内容)。

而对于typeHandler 属性,MyBatis 只支持写在 2 个地方:

  • 定义在 resultMap 里,作用于查询结果的封装
  • 定义在 insert 和 update 语句的 #{property} 中的 property 后面(例:#{property,typehandler=xxx.xxx.xxx}),并且只作用于当前 设置值

除了以上两种直接指定 typeHandler 的形式,MyBatis 有一个全局扫描自定义 typeHandler 包的配置,原理是根据您的 property 类型去找其对应的 typeHandler 并使用。

综上所述,我们对实体类的某个字段自定义了typeHandler,一定要开启autoResultMap=true才能生效,如下所示:

@Data
@TableName(autoResultMap = true)
public class DataSource extends BaseDO implements Serializable {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private String host;
    private String port;
    @TableField(typeHandler = JsonStringSetTypeHandler.class)
    private Set<String> databaseList;
    private String userName;
    @TableField(typeHandler = EncryptTypeHandler.class)
    private String password;
    private Integer type;
    private Integer status;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# @TableId

主键注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
public @interface TableId {

    /**
     * 字段值(驼峰命名方式,该值可无)
     */
    String value() default "";

    /**
     * 主键ID
     * {@link IdType}
     */
    IdType type() default IdType.NONE;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
名称 描述
AUTO 数据库自增ID
NONE 该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
INPUT 用户自己设置的ID
ASSIGN_ID 当用户传入为空时,自动分配类型为Number或String的主键(雪花算法)
ASSIGN_UUID 当用户传入为空时,自动分配类型为String的主键

# @TableField

字段注解(非主键)

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
public @interface TableField {

    /**
     * 数据库字段值
     * <p>
     * 不需要配置该值的情况:
     * <li> 当 {@link com.baomidou.mybatisplus.core.MybatisConfiguration#mapUnderscoreToCamelCase} 为 true 时,
     * (mp下默认是true,mybatis默认是false), 数据库字段值.replace("_","").toUpperCase() == 实体属性名.toUpperCase() </li>
     * <li> 当 {@link com.baomidou.mybatisplus.core.MybatisConfiguration#mapUnderscoreToCamelCase} 为 false 时,
     * 数据库字段值.toUpperCase() == 实体属性名.toUpperCase() </li>
     */
    String value() default "";

    /**
     * 是否为数据库表字段
     * <p>
     * 默认 true 存在,false 不存在
     */
    boolean exist() default true;

    /**
     * 字段 where 实体查询比较条件
     * <p>
     * 默认 {@link SqlCondition#EQUAL}
     */
    String condition() default "";

    /**
     * 字段 update set 部分注入, 该注解优于 el 注解使用
     * <p>
     * 例1:@TableField(.. , update="%s+1") 其中 %s 会填充为字段
     * 输出 SQL 为:update 表 set 字段=字段+1 where ...
     * <p>
     * 例2:@TableField(.. , update="now()") 使用数据库时间
     * 输出 SQL 为:update 表 set 字段=now() where ...
     */
    String update() default "";

    /**
     * 字段验证策略之 insert: 当insert操作时,该字段拼接insert语句时的策略
     * <p>
     * IGNORED: 直接拼接 insert into table_a(column) values (#{columnProperty});
     * NOT_NULL: insert into table_a(<if test="columnProperty != null">column</if>) values (<if test="columnProperty != null">#{columnProperty}</if>)
     * NOT_EMPTY: insert into table_a(<if test="columnProperty != null and columnProperty!=''">column</if>) values (<if test="columnProperty != null and columnProperty!=''">#{columnProperty}</if>)
     * NOT_EMPTY 如果针对的是非 CharSequence 类型的字段则效果等于 NOT_NULL
     *
     * @since 3.1.2
     */
    FieldStrategy insertStrategy() default FieldStrategy.DEFAULT;

    /**
     * 字段验证策略之 update: 当更新操作时,该字段拼接set语句时的策略
     * <p>
     * IGNORED: 直接拼接 update table_a set column=#{columnProperty}, 属性为null/空string都会被set进去
     * NOT_NULL: update table_a set <if test="columnProperty != null">column=#{columnProperty}</if>
     * NOT_EMPTY: update table_a set <if test="columnProperty != null and columnProperty!=''">column=#{columnProperty}</if>
     * NOT_EMPTY 如果针对的是非 CharSequence 类型的字段则效果等于 NOT_NULL
     *
     * @since 3.1.2
     */
    FieldStrategy updateStrategy() default FieldStrategy.DEFAULT;

    /**
     * 字段验证策略之 where: 表示该字段在拼接where条件时的策略
     * <p>
     * IGNORED: 直接拼接 column=#{columnProperty}
     * NOT_NULL: <if test="columnProperty != null">column=#{columnProperty}</if>
     * NOT_EMPTY: <if test="columnProperty != null and columnProperty!=''">column=#{columnProperty}</if>
     * NOT_EMPTY 如果针对的是非 CharSequence 类型的字段则效果等于 NOT_NULL
     *
     * @since 3.1.2
     */
    FieldStrategy whereStrategy() default FieldStrategy.DEFAULT;

    /**
     * 字段自动填充策略
     * <p>
     * 在对应模式下将会忽略 insertStrategy 或 updateStrategy 的配置,等于断言该字段必有值
     */
    FieldFill fill() default FieldFill.DEFAULT;

    /**
     * 是否进行 select 查询
     * <p>
     * 大字段可设置为 false 不加入 select 查询范围
     */
    boolean select() default true;

    /**
     * 是否保持使用全局的 columnFormat 的值
     * <p>
     * 只生效于 既设置了全局的 columnFormat 也设置了上面 {@link #value()} 的值
     * 如果是 false , 全局的 columnFormat 不生效
     *
     * @since 3.1.1
     */
    boolean keepGlobalFormat() default false;

    /**
     * {@link ResultMapping#property} and {@link ParameterMapping#property}
     *
     * @since 3.4.4
     */
    String property() default "";

    /**
     * JDBC类型 (该默认值不代表会按照该值生效),
     * 只生效于 mp 自动注入的 method,
     * 建议配合 {@link TableName#autoResultMap()} 一起使用
     * <p>
     * {@link ResultMapping#jdbcType} and {@link ParameterMapping#jdbcType}
     *
     * @since 3.1.2
     */
    JdbcType jdbcType() default JdbcType.UNDEFINED;

    /**
     * 类型处理器 (该默认值不代表会按照该值生效),
     * 只生效于 mp 自动注入的 method,
     * 建议配合 {@link TableName#autoResultMap()} 一起使用
     * <p>
     * {@link ResultMapping#typeHandler} and {@link ParameterMapping#typeHandler}
     *
     * @since 3.1.2
     */
    Class<? extends TypeHandler> typeHandler() default UnknownTypeHandler.class;

    /**
     * 只在使用了 {@link #typeHandler()} 时判断是否辅助追加 javaType
     * <p>
     * 一般情况下不推荐使用
     * {@link ParameterMapping#javaType}
     *
     * @since 3.4.0 @2020-07-23
     */
    boolean javaType() default false;

    /**
     * 指定小数点后保留的位数,
     * 只生效于 mp 自动注入的 method,
     * 建议配合 {@link TableName#autoResultMap()} 一起使用
     * <p>
     * {@link ParameterMapping#numericScale}
     *
     * @since 3.1.2
     */
    String numericScale() default "";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151

其他注解就不一一叙述,想了解更多直接查看官网文档即可,上面是我们日常开发经常使用的,足够了。

# 4.CRUD 接口

mp封装了一些最基础的CRUD方法,只需要直接继承mp提供的接口,无需编写任何SQL,即可食用。mp提供了两套接口,分别是Mapper CRUD接口和Service CRUD接口。并且mp还提供了条件构造器Wrapper,可以方便地组装SQL语句中的WHERE条件。

# 4.1 Service CRUD 接口

通用 Service CRUD 封装IService接口,进一步封装 CRUD 采用 get 查询单行 remove 删除 list 查询集合 page 分页

首先,新建一个接口,继承IService

public interface UserService extends IService<User> {
}
1
2

创建这个接口的实现类,并继承ServiceImpl

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserDAO, User> implements UserService {

}
1
2
3
4
5

最后使用userSerive就可以调用相关方法, 部分方法截图如下所示:

# 4.2 mapper CRUD 接口

mp将常用的CRUD接口封装成了BaseMapper接口,我们只需要在自己的Mapper中继承它就可以了:

@Mapper
public interface UserDAO extends BaseMapper<User> {

}
1
2
3
4

封装的相关方法部分截图如下:

# 5.条件构造器

mp让我觉得极其方便的一点在于其提供了强大的条件构造器Wrapper,可以非常方便的构造WHERE条件。条件构造器封装抽象类:AbstractWrapper,他是QueryWrapper(LambdaQueryWrapper) 和 UpdateWrapper(LambdaUpdateWrapper) 的父类 用于生成 sql 的 where 条件, entity 属性也用于生成 sql 的 where 条件。

# allEq

allEq(Map<R, V> params)
allEq(Map<R, V> params, boolean null2IsNull)
allEq(boolean condition, Map<R, V> params, boolean null2IsNul
1
2
3

全部eq(或个别isNull)


个别参数说明:
  • params : key为数据库字段名,value为字段值
  • null2IsNull : 为true则在map的value为null时调用 isNull 方法,为false时则忽略value为null的
1: allEq({id:1,name:"老王",age:null})--->id = 1 and name = '老王' and age is null2: allEq({id:1,name:"老王",age:null}, false)--->id = 1 and name = '老王'
1
2
allEq(BiPredicate<R, V> filter, Map<R, V> params)
allEq(BiPredicate<R, V> filter, Map<R, V> params, boolean null2IsNull)
allEq(boolean condition, BiPredicate<R, V> filter, Map<R, V> params, boolean null2IsNull) 
1
2
3

个别参数说明:

  • filter : 过滤函数,是否允许字段传入比对条件中
  • paramsnull2IsNull: 同上
1: allEq((k,v) -> k.contains("a"), {id:1,name:"老王",age:null})--->name = '老王' and age is null2: allEq((k,v) -> k.contains("a"), {id:1,name:"老王",age:null}, false)--->name = '老王'
1
2

# eq

eq(R column, Object val)
eq(boolean condition, R column, Object val)
// 等于 =
1
2
3

# ne

ne(R column, Object val)
ne(boolean condition, R column, Object val)
// 不等于 <>
1
2
3

# gt

gt(R column, Object val)
gt(boolean condition, R column, Object val)
// 大于 >
1
2
3

# ge

ge(R column, Object val)
ge(boolean condition, R column, Object val)
// 大于等于 >=
1
2
3

# lt

lt(R column, Object val)
lt(boolean condition, R column, Object val)
// 小于 <
1
2
3

# le

le(R column, Object val)
le(boolean condition, R column, Object val)
小于等于 <=
1
2
3

# between

between(R column, Object val1, Object val2)
between(boolean condition, R column, Object val1, Object val2)
// BETWEEN 值1 AND 值2
1
2
3

# notBetween

notBetween(R column, Object val1, Object val2)
notBetween(boolean condition, R column, Object val1, Object val2)
// NOT BETWEEN 值1 AND 值2
1
2
3

# like

like(R column, Object val)
like(boolean condition, R column, Object val)
// LIKE '%值%'
1
2
3

# notLike

notLike(R column, Object val)
notLike(boolean condition, R column, Object val)
// NOT LIKE '%值%'
1
2
3

# likeLeft

likeLeft(R column, Object val)
likeLeft(boolean condition, R column, Object val)
//LIKE '%值'
1
2
3

# likeRight

likeRight(R column, Object val)
likeRight(boolean condition, R column, Object val)
//LIKE '值%'
1
2
3

# notLikeLeft

notLikeLeft(R column, Object val)
notLikeLeft(boolean condition, R column, Object val)
NOT LIKE '%值'
1
2
3

# notLikeRight

notLikeRight(R column, Object val)
notLikeRight(boolean condition, R column, Object val)
//NOT LIKE '值%'
1
2
3

# isNull

isNull(R column)
isNull(boolean condition, R column)
//字段 IS NULL
1
2
3

# isNotNull

isNotNull(R column)
isNotNull(boolean condition, R column)
//字段 IS NOT NULL
1
2
3

# in

in(R column, Collection<?> value)
in(boolean condition, R column, Collection<?> value)
//字段 IN (value.get(0), value.get(1), ...)

in(R column, Object... values)
in(boolean condition, R column, Object... values)
//字段 IN (v0, v1, ...)
1
2
3
4
5
6
7

# notIn

notIn(R column, Collection<?> value)
notIn(boolean condition, R column, Collection<?> value)
//字段 NOT IN (value.get(0), value.get(1), ...)

notIn(R column, Object... values)
notIn(boolean condition, R column, Object... values)
//字段 NOT IN (v0, v1, ...)
1
2
3
4
5
6
7

# inSql

inSql(R column, String inValue)
inSql(boolean condition, R column, String inValue)
//字段 IN ( sql语句 )
1
2
3
: inSql("age", "1,2,3,4,5,6")--->age in (1,2,3,4,5,6): inSql("id", "select id from table where id < 3")--->id in (select id from table where id < 3)
1
2

# notInSql

notInSql(R column, String inValue)
notInSql(boolean condition, R column, String inValue)
//字段 NOT IN ( sql语句 )
1
2
3
: notInSql("age", "1,2,3,4,5,6")--->age not in (1,2,3,4,5,6): notInSql("id", "select id from table where id < 3")--->id not in (select id from table where id < 3)
1
2

# groupBy

groupBy(R... columns)
groupBy(boolean condition, R... columns)
//分组:GROUP BY 字段, ...
1
2
3

# orderByAsc

orderByAsc(R... columns)
orderByAsc(boolean condition, R... columns)
//排序:ORDER BY 字段, ... ASC
1
2
3

# orderByDesc

orderByDesc(R... columns)
orderByDesc(boolean condition, R... columns)
//排序:ORDER BY 字段, ... DESC
1
2
3

# orderBy

orderBy(boolean condition, boolean isAsc, R... columns)
//排序:ORDER BY 字段, ...
1
2

# having

having(String sqlHaving, Object... params)
having(boolean condition, String sqlHaving, Object... params)
//HAVING ( sql语句 )
1
2
3
: having("sum(age) > 10")--->having sum(age) > 10: having("sum(age) > {0}", 11)--->having sum(age) > 11
1
2

# func

func(Consumer<Children> consumer)
func(boolean condition, Consumer<Children> consumer)
//func 方法(主要方便在出现if...else下调用不同方法能不断链)
1
2
3
: func(i -> if(true) {i.eq("id", 1)} else {i.ne("id", 1)})
1

# or

or()
or(boolean condition)
//拼接 OR
1
2
3

注意事项:

主动调用or表示紧接着下一个方法不是用and连接!(不调用or则默认为使用and连接)

or(Consumer<Param> consumer)
or(boolean condition, Consumer<Param> consumer)
//OR 嵌套
1
2
3
: or(i -> i.eq("name", "李白").ne("status", "活着"))--->or (name = '李白' and status <> '活着')
1

# and

and(Consumer<Param> consumer)
and(boolean condition, Consumer<Param> consumer)
//AND 嵌套
1
2
3

# nested

nested(Consumer<Param> consumer)
nested(boolean condition, Consumer<Param> consumer)
//正常嵌套 不带 AND 或者 OR
1
2
3
: nested(i -> i.eq("name", "李白").ne("status", "活着"))--->(name = '李白' and status <> '活着')
1

# exists

exists(String existsSql)
exists(boolean condition, String existsSql)
//拼接 EXISTS ( sql语句 )
1
2
3
: exists("select id from table where age = 1")--->exists (select id from table where age = 1)
1

# notExists

notExists(String notExistsSql)
notExists(boolean condition, String notExistsSql)
//拼接 NOT EXISTS ( sql语句 )
1
2
3

# QueryWrapper

说明:

继承自 AbstractWrapper ,自身的内部属性 entity 也用于生成 where 条件 及 LambdaQueryWrapper, 可以通过 new QueryWrapper().lambda() 方法获取

# select

select(String... sqlSelect)
select(Predicate<TableFieldInfo> predicate)
select(Class<T> entityClass, Predicate<TableFieldInfo> predicate)
//设置查询字段
1
2
3
4

说明:

以上方法分为两类. 第二类方法为:过滤查询字段(主键除外),入参不包含 class 的调用前需要wrapper内的entity属性有值! 这两类方法重复调用以最后一次为准

: select("id", "name", "age"): select(i -> i.getProperty().startsWith("test"))
1
2
3

# UpdateWrapper

说明:

继承自 AbstractWrapper ,自身的内部属性 entity 也用于生成 where 条件 及 LambdaUpdateWrapper, 可以通过 new UpdateWrapper().lambda() 方法获取!

# set

set(String column, Object val)
set(boolean condition, String column, Object val)
//SQL SET 字段
1
2
3
: set("name", "老李头"): set("name", "")--->数据库字段值变为空字符串
例: set("name", null)--->数据库字段值变为null
1
2
3

# lambda

获取 LambdaWrapper在QueryWrapper中是获取LambdaQueryWrapper 在UpdateWrapper中是获取LambdaUpdateWrapper

# 分页

使用分页话需要增加分页插件的配置

@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

}
1
2
3
4
5
6
7
8
9
10
11

最后来个示例看看上面的条件构造器是怎么用的:

    @Override
    public List<BrandDTO> getList(BrandQuery query) {
        LambdaQueryWrapper<Brand> queryWrapper = new LambdaQueryWrapper<>();
        if (query.getCategoryId() != null) {
            queryWrapper.eq(Brand::getCategoryId, query.getCategoryId());
        }
        if (StringUtils.isNotBlank(query.getLetter())) {
            queryWrapper.eq(Brand::getLetter, query.getLetter());
        }
        if (StringUtils.isNotBlank(query.getName())) {
            queryWrapper.likeRight(Brand::getName, query.getName());
        }
        List<BrandDTO> brandDTOList = brandDAO.selectList(queryWrapper).stream().map(brand -> toBrandDTO(brand)).collect(Collectors.toList());
        return brandDTOList;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15