MyBatis-Plus实战(Spring Boot集成)

1.为何MyBatis-Plus

在Spring框架风靡java开发的今天,java应用的持久层框架主要有MyBatisJPA(Spring Data JPA)。熟悉这两个框架的开发者很容易就能总结出以下结论:

  • MyBatis优势

    • SQL语句可以自由控制,更灵活,性能较高。
    • SQL与代码分离,易于阅读和维护
    • 提供XML标签,支持编写动态SQL语句
  • JPA优势

    • JPA移植性比较好(JPQL)
    • 提供了很多CRUD方法,开发效率高
    • 对象化程度更高

对比这两个框架的优势,也可以很容易发现MyBatis的劣势:

  • MyBatis劣势
    • 简单CRUD操作还得写SQL语句
    • 项目中有大量SQL需要维护
    • MyBatis自身功能很有限,但支持Plugin

那么,有没有一种框架可以结合这两个框架的优势,使得我们开发应用的效率可以大大提高,又可以灵活、好维护呢?答案正是我们今天要讨论的主角:MyBatis-Plus

MyBatis-Plus 荣获【2018年度开源中国最受欢迎的中国软件】 TOP5 :

本文将以一个简单的例子来讲解MyBatis-Plus在项目开发中的应用。

2.何为MyBatis-Plus

MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

MyBatis-Plus官方的愿景是成为 MyBatis 最好的搭档,就像 魂斗罗 中的 1P、2P,基友搭配,效率翻倍。

MyBatis-Plus实战(Spring Boot集成)_第1张图片

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 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 操作智能分析阻断,也可自定义拦截规则,预防误操作

以下是MP的框架结构:

MyBatis-Plus实战(Spring Boot集成)_第2张图片

3.Spring Boot集成

在Spring Boot中集成MP是一件非常简单的事,只需要引入MP官方提供的starter依赖即可:

<dependency>
    <groupId>com.baomidougroupId>
    <artifactId>mybatis-plus-boot-starterartifactId>
    <version>3.2.0version>
dependency>
  • 注:引入 MyBatis-Plus 之后请不要再次引入 MyBatis 以及 MyBatis-Spring,以避免因版本差异导致的问题。
  • 全新的 MyBatis-Plus 3.0 版本基于 JDK8,提供了 lambda 形式的调用,所以安装集成 MP3.0 要求如下:
    • JDK 8+
    • Maven or Gradle
  • 当然,数据库连接用到的connector以及数据源spring.datasource的配置也是必不可少的。

4.MP实战

在正式进入实战之前,我们先设计一套用来操作的数据库:

现有一张Team表,其表数据如下:

id name city
1 Lakers Los Angeles
2 Warriors Golden State
3 Raptors Toronto
4 Bucks Milwaukee
5 Spurs San Antonio

对应的数据库Schema脚本如下:

DROP TABLE IF EXISTS team;

CREATE TABLE team
(
    id BIGINT(20) NOT NULL COMMENT '主键ID',
    name VARCHAR(30) NULL DEFAULT NULL COMMENT '球队名称',
    city VARCHAR(30) NULL DEFAULT NULL COMMENT '所在城市',
    PRIMARY KEY(id)
);

对应的数据库 Data 脚本如下:

DELETE FROM team;

INSERT INTO team(id, name, city) VALUES
(1, 'Lakers', 'Los Angeles'),
(2, 'Warriors', 'Golden State'),
(3, 'Raptors', 'Toronto'),
(4, 'Bucks', 'Milwaukee'),
(5, 'Spurs', 'San Antonio');

另外,用作多表关联实战,还有一张Player表,其表部分数据如下:

id team_id name no
1001 1 Rejon Rondo 9
2001 2 Stephen Curry 30
3001 3 Kyle Lowry 7
4001 4 George Hill 3
5001 5 Tony Parker 9

对应的数据库Schema脚本如下:

DROP TABLE IF EXISTS player;

CREATE TABLE player
(
    id BIGINT(20) NOT NULL COMMENT '主键ID',
    team_id BIGINT(20) DEFAULT NULL COMMENT '所属球队ID',
    name VARCHAR(50) DEFAULT NULL COMMENT '球员名称',
    no INT(3) DEFAULT NULL COMMENT '球员号码',
    PRIMARY KEY(id)
);

对应的数据库 Data 脚本如下:

DELETE FROM player;

INSERT INTO player(id, team_id, name, no) VALUES
(1001, 1, 'Rejon Rondo', 9),
(1002, 1, 'Danny Green', 14),
(1003, 1, 'LeBron James', 23),
(1004, 1, 'Anthony Davis', 3),
(1005, 1, 'Dwight Howard', 39),
(1006, 1, 'Kyle Kuzma', 0),
(1007, 1, 'DeMarcus Cousins', 15),
(1008, 1, 'JaVale McGee', 7),
(1009, 1, 'Jared Dudley', 10),
(1010, 1, 'Kentavious Caldwell-Pope', 1),
(1011, 1, 'Avery Bradley', 11),
(1012, 1, 'Quinn Cook', 2),

(2001, 2, 'Stephen Curry', 30),
(2002, 2, 'Angelo Russell', 0),
(2003, 2, 'Klay Thompson', 11),
(2004, 2, 'Draymond Green', 23),
(2005, 2, 'Willie Cauley Stein', 2),

(3001, 3, 'Kyle Lowry', 7),
(3002, 3, 'Fan Jordon', 23),
(3003, 3, 'Pascal Siakam', 43),
(3004, 3, 'Serge Ibaka', 9),
(3005, 3, 'Marc Gasol', 33),

(4001, 4, 'George Hill', 3),
(4002, 4, 'Eric Bledsoe', 6),
(4003, 4, 'Khris Middleton', 22),
(4004, 4, 'The Alphabet', 34),
(4005, 4, 'Robin Lopez', 42),

(5001, 5, 'Tony Parker', 9),
(5002, 5, 'Patty Mills', 8),
(5003, 5, 'Manu Ginobili', 20),
(5004, 5, 'Boris Diaw', 33),
(5005, 5, 'Tim Duncan', 21),

(0001, NULL, 'Carmelo Anthony', NULL),
(0002, NULL, 'JR Smith', NULL),
(0003, NULL, 'Kenneth Faried', NULL),
(0004, NULL, 'Tyreke Evans', NULL),
(0005, NULL, 'Joakim Noah', NULL);

这两张表很简单,Player表通过tearm_id属性外键关联Team表。最后还需要在我们的Spring Boot准备这两个实体的DO类:

public class Team {

    private Long id;
    private String name;
    private String city;

    // getters and setters
}

public class Player {

    private Long id;
    private Long teamId;
    private String name;
    private Integer no;

    // getters and setters
}
  • 建议在项目中引入Lombok框架,可以省去定义pojogettersetter的繁琐工作,代码也更简洁。

4.1通用Mapper - CRUD接口

为了可以看到MP操作数据库时使用的SQL语句,需要把mapper包或者项目日志级别logging.level配置为TRACE

说明:

  • 通用 CRUD 封装BaseMapper接口,为 Mybatis-Plus 启动时自动解析实体表关系映射转换为 Mybatis 内部对象注入容器
  • 泛型 T 为任意实体对象
  • 参数 Serializable 为任意类型主键 Mybatis-Plus 不推荐使用复合主键约定每一张表都有自己的唯一 id 主键
  • 对象 Wrapper 为 条件构造器

DAO层的数据库操作接口通过继承通用Mappercom.baomidou.mybatisplus.core.mapper.BaseMapper来使用MP提供的CRUD能力,使得我们可以不用写SQL语句,更简便的操作数据库。我们先来看看**通用Mapper(BaseMapper)**的源码:

MyBatis-Plus实战(Spring Boot集成)_第3张图片

可以看到通用Mapper提供了很多应对各种业务需求的数据库操作,现在我们先创建之前提到的两个DO类对应的DAO操作类:

public interface TeamMapper extends BaseMapper<Team> {
}

public interface PlayerMapper extends BaseMapper<Player> {
}

非常简单,我们现在一行代码都不需要。接下来我们还需要在Spring Boot启动类中使用@MapperScan配置Mapper包扫描。下面我们对Mapper的操作进行一一介绍。

4.1.1C - Insert操作

  • Insert相关方法:
    • int insert(T entity);// 插入一条记录

现在我们直接使用单元测试的方式给Team表插入一个新的球队:

@SpringBootTest
class TeamMapperTest {

    @Autowired
    private TeamMapper teamMapper;

    @Test
    public void testInsert() {
        Team team = new Team();
        team.setName("Cavaliers");
        team.setCity("Cleveland");
        int result = teamMapper.insert(team);
        Assertions.assertEquals(1, result);
    }

}
  • 如果单元测试成功,说明我们的Spring Boot集成MyBatis-Plus环境成功。

我们现在查看数据库,Team表中确实多了一条记录,但是id这一列是一长串数字(当然你运行的结果跟我的很大概率是不一样的数字),即使你设置了表主键是自增长策略,依然是这个情况。

MyBatis-Plus实战(Spring Boot集成)_第4张图片

这是因为MP的默认主键策略起的作用。

MP主键策略

描述
AUTO 数据库自增
INPUT 自行输入
ID_WORKER 分布式全局唯一ID 长整型类型
UUID 32位UUID字符串
NONE 无状态
ID_WORKER_STR 分布式全局唯一ID 字符串类型

MP默认的主键策略是ID_WORKER,所以我们Insert操作生成的数据id为一串数字。要修改MP的主键策略,可以通过全局局部的配置。

全局配置主键是MP配置项(MP配置大全)的一部分,通过在Spring Boot的配置文件中配置mybatis-plus.global-config.db-config.id-type即可。

局部配置则是通过一个成员变量注解@TableId来实现,以下是该注解的描述:

属性 类型 必须指定 默认值 描述
value String “” 主键字段名
type Enum IdType.NONE 主键类型
  • TableId注解除了声明Id策略的type外,还有一个名为value的属性,这个属性定义的是当我们的数据表实体的主键名。在MP中,默认的数据库主键名为id,如果我们表实体主键不是起名为id,那么我们就需要配置这个属性。

我们可以看到,声明的DO类默认的Id策略是NONE,这个时候就受全局配置的影响。我们可以尝试把MP全局主键策略配置为AUTO,即自增长策略注意,这时也需要数据表启用自增长策略

  • 对于Insert操作,MP默认会把entity所有的属性转换成数据表的列来插入,如果设计的DO类中有非表字段的属性,那么insert操作就会报错,为了避免这种情况,可以在非数据表字段的属性添加@TableField(exist = false)注解来设置非数据库表字段。

4.1.2R - Select操作

  • Select相关方法:
    • T selectById(Serializable id);// 根据 ID 查询
    • List selectBatchIds(@Param(Constants.COLLECTION) Collection idList);// 根据 ID 查询
    • List selectByMap(@Param(Constants.COLUMN_MAP) Map columnMap);// 查询(根据 columnMap 条件)
    • T selectOne(@Param(Constants.WRAPPER) Wrapper queryWrapper);// 根据 entity 条件,查询一条记录
    • Integer selectCount(@Param(Constants.WRAPPER) Wrapper queryWrapper);// 根据 Wrapper 条件,查询总记录数
    • List selectList(@Param(Constants.WRAPPER) Wrapper queryWrapper);// 根据 entity 条件,查询全部记录
    • List> selectMaps(@Param(Constants.WRAPPER) Wrapper queryWrapper);// 根据 Wrapper 条件,查询全部记录
    • List selectObjs(@Param(Constants.WRAPPER) Wrapper queryWrapper);// 根据 Wrapper 条件,查询全部记录。注意: 只返回第一个字段的值
    • IPage selectPage(IPage page, @Param(Constants.WRAPPER) Wrapper queryWrapper);// 根据 entity 条件,查询全部记录(并翻页)
    • IPage> selectMapsPage(IPage page, @Param(Constants.WRAPPER) Wrapper queryWrapper);// 根据 Wrapper 条件,查询全部记录(并翻页)
    • MP中的SelectDelete都提供了多种操作,总的来说可以分成根据Id操作根据columnMap操作(Map设置表字段和值)根据Wrapper操作(Wrapper是MP提供的条件构造器,可以实现很强大的功能)。上面最后两个是关于分页的查询,这部分将在本文后面进行讲解。

      根据Id和根据columnMap操作相对比较简单,实现的功能也相对有限,这里先看几个例子:

      @Test
      public void testSelectById() {
          // 查找Id为1的球队
          Team team = teamMapper.selectById(1L);
          // MP执行的SQL:
          // Preparing: SELECT id,city,name FROM team WHERE id=?
          // Parameters: 1(Long)
          Assertions.assertNotNull(team);
      
          // 查找Id为1、2、3的球队
          List<Team> teamList = teamMapper.selectBatchIds(Arrays.asList(1L, 2L, 3L));
          // MP执行的SQL:
          // Preparing: SELECT id,city,name FROM team WHERE id IN ( ? , ? , ? )
          // Parameters: 1(Long), 2(Long), 3(Long)
          Assertions.assertNotEquals(0, teamList.size());
      }
      
      @Test
      public void testSelectByMap() {
          // 查找洛杉矶的球队
          Map<String, Object> columnMap = new HashMap<>();
          columnMap.put("city", "Los Angeles");   // Key值为数据库列名
          List<Team> teamList = teamMapper.selectByMap(columnMap);
          // MP执行的SQL:
          // Preparing: SELECT id,city,name FROM team WHERE city = ?
          // Parameters: Los Angeles(String)
          Assertions.assertNotEquals(0, teamList.size());
      }
      

      如果想要实现复杂的操作比如聚合、投影等操作,就要使用高级的**条件构造器(Wrapper)**方法了。

      条件构造器

      MP关于条件构造器主要涉及到AbstractWrapperQueryWrapper(LambdaQueryWrapper)UpdateWrapper(LambdaUpdateWrapper)

      • AbstractWrapper
        • QueryWrapper(LambdaQueryWrapper)UpdateWrapper(LambdaUpdateWrapper)的父类,用于生成sqlwhere条件,entity属性也用于生成sql的where条件。
        • 注意:entity生成的where条件与使用各个api生成的where条件没有任何关联行为
      • QueryWrapper
        • 继承自AbstractWrapper,自身的内部属性entity也用于生成where条件,LambdaQueryWrapper可以通过new QueryWrapper().lambda()方法获取。
      • UpdateWrapper
        • 继承自AbstractWrapper,自身的内部属性entity也用于生成where条件,LambdaUpdateWrapper可以通过new UpdateWrapper().lambda()方法获取。

      条件构造器包含了基本上所有的SQL操作,光看概念会有点抽象,接下来我们用一些实际的例子来近距离感受下条件构造器的强大功能。

      一些例子
      @SpringBootTest
      class PlayerMapperTest {
      
          @Autowired
          private PlayerMapper playerMapper;
      
          /*
          1.球员姓名包含Kyle且号码小于40的球员
          name LIKE '%Kyle%' AND no < 40
           */
          @Test
          public void testQuery1() {
              QueryWrapper<Player> queryWrapper = new QueryWrapper<>();
      
              queryWrapper.like("name", "Kyle").lt("no", 40);
      
              List<Player> playerList = playerMapper.selectList(queryWrapper);
              // MP执行的SQL:
              // Preparing: SELECT id,no,team_id,name FROM player WHERE (name LIKE ? AND no < ?)
              // Parameters: %Kyle%(String), 40(Integer)
              Assertions.assertNotEquals(0, playerList.size());
          }
      
          /*
          2.球员姓名包含Kyle且号码大于等于0且小于等于20并且team_id不为空
          name LIKE '%Kyle%' AND no BETWEEN 20 AND 40 AND team_id IS NOT NULL
           */
          @Test
          public void testQuery2() {
              QueryWrapper<Player> queryWrapper = new QueryWrapper<>();
      
              queryWrapper.like("name", "Kyle")
                          .between("no", 0, 20)
                          .isNotNull("team_id");
      
              List<Player> playerList = playerMapper.selectList(queryWrapper);
              // MP执行的SQL:
              // Preparing: SELECT id,no,team_id,name FROM player WHERE (name LIKE ? AND no BETWEEN ? AND ? AND team_id IS NOT NULL)
              // Parameters: %Kyle%(String), 0(Integer), 20(Integer)
              Assertions.assertNotEquals(0, playerList.size());
          }
      
          /*
          3.球员姓名以Anthony开头或者球员号码大于等于25,按照号码降序排序,号码相同按照team_id升序排序
          name LIKE 'Anthony%' OR no >= 25 ORDER BY no DESC, team_id ASC
           */
          @Test
          public void testQuery3() {
              QueryWrapper<Player> queryWrapper = new QueryWrapper<>();
      
              queryWrapper.likeRight("name", "Anthony")   // 以 LIKE 'X%' 形式
                          .or().gt("no", 25)  // 调用 .or() 即可以 or 形式连接
                          .orderByDesc("no")
                          .orderByAsc("team_id");
      
              List<Player> playerList = playerMapper.selectList(queryWrapper);
              // MP执行的SQL:
              // Preparing: SELECT id,no,team_id,name FROM player WHERE (name LIKE ? OR no > ?) ORDER BY no DESC , team_id ASC
              // Parameters: Anthony%(String), 25(Integer)
              Assertions.assertNotEquals(0, playerList.size());
          }
      
          /*
          4.球员姓名包含Anthony并且所属球队为Lakers
          name LIKE '%Anthony%' AND team_id IN (SELECT id FROM team WHERE name = 'Lakers')
           */
          @Test
          public void testQuery4() {
              QueryWrapper<Player> queryWrapper = new QueryWrapper<>();
      
              queryWrapper.like("name", "Anthony")
                          .inSql("team_id", "SELECT id FROM team WHERE name = 'Lakers'"); // in 操作
      
              List<Player> playerList = playerMapper.selectList(queryWrapper);
              // MP执行的SQL:
              // Preparing: SELECT id,no,team_id,name FROM player WHERE (name LIKE ? AND team_id IN (SELECT id FROM team WHERE name = 'Lakers'))
              // Parameters: %Anthony%(String)
              Assertions.assertNotEquals(0, playerList.size());
          }
      
          /*
          5.球员姓名包含Kyle并且(球员号码小于40或者team_id不为空)
          name LIKE '%Kyle%' AND (no < 40 OR team_id IS NOT NULL)
           */
          @Test
          public void testQuery5() {
              QueryWrapper<Player> queryWrapper = new QueryWrapper<>();
      
              queryWrapper.like("name", "Kyle")
                          // .and(qw -> qw.lt("no", 40).or().isNotNull("team_id"))    // AND 嵌套
                          .nested(qw -> qw.lt("no", 40).or().isNotNull("team_id"));   // 正常嵌套 不带 AND 或者 OR
      
              List<Player> playerList = playerMapper.selectList(queryWrapper);
              // MP执行的SQL:
              // Preparing: SELECT id,no,team_id,name FROM player WHERE (name LIKE ? AND ( (no < ? OR team_id IS NOT NULL) ))
              // Parameters: %Kyle%(String), 40(Integer)
              Assertions.assertNotEquals(0, playerList.size());
          }
      
          /*
          6.球员姓名以Kyle开头或者(球员号码小于40并且号码大于0并且team_id不为空)
          name LIKE 'Kyle%' AND (no < 40 AND no > 0 AND team_id IS NOT NULL)
           */
          @Test
          public void testQuery6() {
              QueryWrapper<Player> queryWrapper = new QueryWrapper<>();
      
              queryWrapper.likeRight("name", "Kyle")
                      // .or(qw -> qw.lt("no", 40).gt("no", 0).isNotNull("team_id"))    // OR 嵌套
                      .or().nested(qw -> qw.lt("no", 40).gt("no", 0).isNotNull("team_id"));   // 正常嵌套 不带 AND 或者 OR
      
              List<Player> playerList = playerMapper.selectList(queryWrapper);
              // MP执行的SQL:
              // Preparing: SELECT id,no,team_id,name FROM player WHERE (name LIKE ? OR ( (no < ? AND no > ? AND team_id IS NOT NULL) ))
              // Parameters: Kyle%(String), 40(Integer), 0(Integer)
              Assertions.assertNotEquals(0, playerList.size());
          }
      
          /*
          7.(球衣号码小于40或team_id不为空)并且姓名以LeBron开头
          (no < 40 OR team_id IS NOT NULL) AND name LIKE 'LeBron%'
           */
          @Test
          public void testQuery7() {
              QueryWrapper<Player> queryWrapper = new QueryWrapper<>();
      
              queryWrapper.nested(qw -> qw.lt("no", 40).or().isNotNull("team_id"))
                          .likeRight("name", "LeBron");
      
              List<Player> playerList = playerMapper.selectList(queryWrapper);
              // MP执行的SQL:
              // Preparing: SELECT id,no,team_id,name FROM player WHERE (( (no < ? OR team_id IS NOT NULL) ) AND name LIKE ?)
              // Parameters: 40(Integer), LeBron%(String)
              Assertions.assertNotEquals(0, playerList.size());
          }
      
          /*
          8.球员号码为3, 13, 23, 33
          no in (3, 13, 23, 33)
           */
          @Test
          public void testQuery8() {
              QueryWrapper<Player> queryWrapper = new QueryWrapper<>();
      
              queryWrapper.in("no", Arrays.asList(3, 13, 23, 33));
      
              List<Player> playerList = playerMapper.selectList(queryWrapper);
              // MP执行的SQL:
              // Preparing: SELECT id,no,team_id,name FROM player WHERE (no IN (?,?,?,?))
              // Parameters: 3(Integer), 13(Integer), 23(Integer), 33(Integer)
              Assertions.assertNotEquals(0, playerList.size());
          }
      
          /*
          9.limit
          LIMIT 1
           */
          @Test
          public void testQuery9() {
              QueryWrapper<Player> queryWrapper = new QueryWrapper<>();
      
              queryWrapper.in("no", Arrays.asList(3, 13, 23, 33))
                          .last("limit 1");   // 无视优化规则直接拼接到 sql 的最后;只能调用一次,多次调用以最后一次为准 有sql注入的风险,请谨慎使用
      
              List<Player> playerList = playerMapper.selectList(queryWrapper);
              // MP执行的SQL:
              // Preparing: SELECT id,no,team_id,name FROM player WHERE (no IN (?,?,?,?)) limit 1
              // Parameters: 3(Integer), 13(Integer), 23(Integer), 33(Integer)
              Assertions.assertNotEquals(0, playerList.size());
          }
      
          /*
          10.select
          SELECT id, name
          FROM player
           */
          @Test
          public void testQuery10() {
              QueryWrapper<Player> queryWrapper = new QueryWrapper<>();
      
              queryWrapper.select(Player.class, i -> i.getColumn().equals("id")||i.getColumn().equals("name"))    // 投影操作,指定返回的列
                          //.select("id", "name")   // 投影操作,指定返回的列
                          .in("no", Arrays.asList(3, 13, 23, 33));
      
              List<Player> playerList = playerMapper.selectList(queryWrapper);
              // MP执行的SQL:
              // Preparing: SELECT id,name FROM player WHERE (no IN (?,?,?,?))
              // Parameters: 3(Integer), 13(Integer), 23(Integer), 33(Integer)
              Assertions.assertNotEquals(0, playerList.size());
          }
      
          /*
          11.使用统计函数
          SELECT team_id, AVG(no) avg_no, MAX(no) max_no
          FROM player
          GROUP BY team_id
          HAVING SUM(no) < 500
           */
          @Test
          public void testQuery11() {
              QueryWrapper<Player> queryWrapper = new QueryWrapper<>();
      
              queryWrapper.select("team_id", "AVG(no) avg_no", "MAX(no) max_no")  // select 可以直接使用聚合函数并且起别名
                          .groupBy("team_id") // group by
                          .having("max_no < 500");   // 通过 sql 指定 having
      
              List<Player> playerList = playerMapper.selectList(queryWrapper);
              // MP执行的SQL:
              // Preparing: SELECT team_id,AVG(no) avg_no,MAX(no) max_no FROM player GROUP BY team_id HAVING SUM(no) < 500
              // Parameters:
              Assertions.assertNotEquals(0, playerList.size());
          }
      
      }
      

      MP设计的方法大多都是名如其意,而且使用非常简单和优雅,多操作几遍很快就能上手。另外,通用Mapper还提供了selectOneselectMaps等方法,大家可以自行设计应用场景来熟悉我们没有使用到的方法,调用方法都类似。

      另外,这里再介绍一个更优雅的查询方式,使用QueryChainWrapper进行操作,一步到位:

      @Test
      public void testQueryChain() {
          QueryChainWrapper<Player> queryChainWrapper = new QueryChainWrapper<>(playerMapper);
          List<Player> playerList = queryChainWrapper.eq("team_id", 1)
              .list();    // 其底层是调用了 BaseMapper.selectList
          Assertions.assertNotEquals(0, playerList.size());
          // MP执行的SQL:
          // Preparing: SELECT team_id,AVG(no) avg_no,MAX(no) max_no FROM player GROUP BY team_id HAVING max_no < 500
          // Parameters:
      }
      

      之前还提到了LambdaQueryWrapper,使用方法跟QueryWrapper类似,而且LambdaQueryWrapper可以定制查询的列:

      @Test
      public void testQueryLambda() {
          // 三种构建 LambdaQueryWrapper 的方法:
          //LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
          //LambdaQueryWrapper lambdaQueryWrapper = new QueryWrapper().lambda();
          LambdaQueryWrapper<Player> lambdaQueryWrapper = Wrappers.lambdaQuery();
          
          lambdaQueryWrapper.eq(Player::getTeamId, 1L);
          List<Player> playerList = playerMapper.selectList(lambdaQueryWrapper);
          // MP执行的SQL:
          // Preparing: SELECT id,no,team_id,name FROM player WHERE (team_id = ?)
          // Parameters: 1(Long)
          Assertions.assertNotEquals(0, playerList.size());
      
          LambdaQueryChainWrapper<Player> lambdaQueryChainWrapper = new LambdaQueryChainWrapper<>(playerMapper);
          playerList = lambdaQueryChainWrapper.eq(Player::getTeamId, 1L).list();
          // MP执行的SQL:
          // Preparing: SELECT id,no,team_id,name FROM player WHERE (team_id = ?)
          // Parameters: 1(Long)
          Assertions.assertNotEquals(0, playerList.size());
      }
      

      不管是QueryWrapper还是LambdaQueryWrapper,除了上面提到的构造方法外,都有对应的传入一个entity的构造方法,这时候MP在构造WHERE时会把entity的非空属性以AND的形式直接添加,并且与条件构造器本身设置的条件不冲突。但是以entity的形式只是简单的用=条件,例如:

      @Test
      public void testQueryEntity() {
          Player player = new Player();
          player.setName("James");
      
          QueryWrapper<Player> queryWrapper = new QueryWrapper<>(player);
          queryWrapper.eq("team_id", 1L);
          List<Player> playerList = playerMapper.selectList(queryWrapper);
          // MP执行的SQL:
          // Preparing: SELECT id,no,team_id,name FROM player WHERE name=? AND (team_id = ?)
          // Parameters: James(String), 1(Long)
          Assertions.assertNotEquals(0, playerList.size());
      }
      

      这段单元测试会失败,因为playerList.size()为0,这是由于查询条件name使用了=,如果我们希望nameLIKE的形式来构建查询,那么我们可以在name属性上设置@TableField(condition = SqlCondition.LIKE),这时单元测试就跟我们预想的一样了。

      4.1.3U - Update操作

      • Update相关方法:
        • updateById// 根据 ID 修改
        • update// 根据 whereEntity 条件,更新记录

      MP的Update操作都是基于entityentity的非空属性将会成为UPDATE的字段(id除外)。其中updateById只接收一个entity参数,并且id属性不能为空。而update方法除了接收需要修改的数据entity之外,还要传入一个Wrapper对象,一般Update操作使用UpdateWrapper,这个Wrapper对象可以指定需要Update的记录:

      @Test
      public void testUpdate() {
          Player player = new Player();
          player.setNo(0);
          UpdateWrapper<Player> updateWrapper = new UpdateWrapper<>();
          updateWrapper.isNull("team_id");
          int result = playerMapper.update(player, updateWrapper);
          // MP执行的SQL:
          // Preparing: UPDATE player SET no=? WHERE (team_id IS NULL)
          // Parameters: 0(Integer)
          Assertions.assertNotEquals(0, result);
      }
      

      有了之前QueryWrapper的基础,现在使用UpdateWrapper就信手拈来了。但是我们发现这样使用UpdateWrapper并没有使用到UpdateWrapper的特性,其实UpdateWrapper有关于Update的方法:

      @Test
      public void testUpdateSet() {
          UpdateWrapper<Player> updateWrapper = new UpdateWrapper();
          updateWrapper.isNull("team_id").set("no", 100);
          int result = playerMapper.update(null, updateWrapper);
          // MP执行的SQL:
          // Preparing: UPDATE player SET no=? WHERE (team_id IS NULL)
          // Parameters: 0(Integer)
          Assertions.assertNotEquals(0, result);
      }
      

      当通过UpdateWrapper进行SET操作时,entity就可以传入一个null值了。

      4.1.4D - Delete操作

      • Delete相关方法:
        • int deleteById(Serializable id); // 根据 ID 删除
        • int deleteByMap(@Param(Constants.COLUMN_MAP) Map columnMap);// 根据 columnMap 条件,删除记录
        • int delete(@Param(Constants.WRAPPER) Wrapper wrapper);// 根据 entity 条件,删除记录
        • int deleteBatchIds(@Param(Constants.COLLECTION) Collection idList);// 删除(根据ID 批量删除)

      Delete操作涉及的方法相对简单,直接通过Id或者构建WHERE即可。

      4.2通用Service

      MP的另一大特色是框架为我们提供了通用Service,使得我们只需关注系统业务。

      说明:

      • 通用 Service CRUD 封装IService接口,进一步封装 CRUD 采用 get 查询单行remove 删除list 查询集合page 分页 前缀命名方式区分 Mapper 层避免混淆。
      • 泛型 T 为任意实体对象
      • 建议如果存在自定义通用 Service 方法的可能,请创建自己的 IBaseService 继承 Mybatis-Plus 提供的基类
      • 对象 Wrapper 为 条件构造器

      通用Service的操作跟通用Mapper类似,区别是按照Service层规范把DAO层的insertselectdelete改成了saveget\listremove

      现在我们也创建Service层的接口和实现类:

      public interface TeamService extends IService<Team> {
      }
      
      @Service
      @Transactional
      public class TeamServiceImpl extends ServiceImpl<TeamMapper, Team> implements TeamService {
      }
      
      public interface PlayerService extends IService<Player> {
      }
      
      @Service
      @Transactional
      public class PlayerServiceImpl extends ServiceImpl<PlayerMapper, Player> implements PlayerService {
      }
      

      使用通用Service非常简单,声明接口时继承IService就可以拥有MP提供的通用Service的能力;实现类除了实现接口外还需要继承MP的ServiceImpl类,并传入通用Mapper和对应的DO对象。

      下面我们直接通过几个例子来使用MP的通用Service

      @SpringBootTest
      class TeamServiceTest {
      
          @Autowired
          TeamService teamService;
      
          @Test
          public void testSave() {
              Team team = new Team();
              team.setName("Nuggets");
              team.setCity("Denver");
              boolean result = teamService.save(team);
              Assertions.assertTrue(result);
      
              Team team1 = new Team();
              team1.setName("Grizzlies");
              team1.setCity("Memphis");
              Team team2 = new Team();
              team2.setName("NETS");
              team2.setCity("Brooklyn");
              Team team3 = new Team();
              team3.setName("Thunder");
              team3.setCity("Oklahoma");
              result = teamService.saveBatch(Arrays.asList(team1, team2, team3));
              Assertions.assertTrue(result);
          }
      
          @Test
          public void testSelect() {
              Team team = teamService.getOne(Wrappers.<Team>query().eq("id", 1L), false);
              Assertions.assertNotNull(team);
              // MP执行的SQL:
              // Preparing: SELECT id,city,name FROM team WHERE (id = ?)
              // Parameters: 1(Long)
      
              List<Team> teamList = teamService.list(Wrappers.<Team>query().le("id", 5L));
              Assertions.assertNotEquals(0, teamList.size());
              // MP执行的SQL:
              // Preparing: SELECT id,city,name FROM team WHERE (id <= ?)
              // Parameters: 5(Long)
          }
      
          @Test
          public void testUpdate() {
              Team team = new Team();
              team.setId(1L);
              team.setName("Laker");
              boolean result = teamService.saveOrUpdate(team);    // saveOrUpdate 会先执行 SELECT BY Id 操作,如果找到有记录则进行 UPDATE 操作,否则进行 INSERT 操作
              Assertions.assertTrue(result);
              // MP执行的SQL:
              // Preparing: SELECT id,city,name FROM team WHERE id=?
              // Parameters: 1(Long)
              // Preparing: UPDATE team SET name=? WHERE id=?
              // Parameters: Laker(String), 1(Long)
      
              team = new Team();
              team.setId(100L);
              team.setName("Blazers");
              team.setCity("Portland");
              result = teamService.saveOrUpdate(team);
              Assertions.assertTrue(result);
              // MP执行的SQL:
              // Preparing: SELECT id,city,name FROM team WHERE id=?
              // Parameters: 100(Long)
              // Preparing: INSERT INTO team ( id, city, name ) VALUES ( ?, ?, ? )
              // Parameters: 100(Long), Portland(String), Blazers(String)
      
              teamService.update(Wrappers.<Team>update().eq("id", 1L).set("name", "Lakers"));
              // MP执行的SQL:
              // Preparing: UPDATE team SET name=? WHERE (id = ?)
              // Parameters: Lakers(String), 1(Long)
          }
      
      }
      

      我们可以看到,通用Service通用Mapper操作方式基本保持一致,需要注意的是,通用Servicesaveupdate操作返回值是boolean类型。另外,我们还可以用链式操作以更优雅的方式进行操作:

      @Test
      public void testChain() {
          List<Team> teamList = teamService.query().select("id", "name").le("id", 5L).list();
          Assertions.assertNotEquals(0, teamList.size());
          // MP执行的SQL:
          // Preparing: SELECT id,name FROM team WHERE (id <= ?)
          // Parameters: 5(Long)
      }
      

      4.3分页查询

      在实际项目中用得最多也比较重要的业务应该就是分页操作了,MP对分页操作也做了插件支持。

      在Spring Boot项目中使用MP的分页功能也非常简单,只需要在容器中注入PaginationInterceptor对象即可。这里以@Configuration配置:

      @EnableTransactionManagement
      @Configuration
      public class MyBatisPlusConfig {
          @Bean
          public PaginationInterceptor paginationInterceptor() {
              PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
              // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
              // paginationInterceptor.setOverflow(false);
              // 设置最大单页限制数量,默认 500 条,-1 不受限制
              // paginationInterceptor.setLimit(500);
              return paginationInterceptor;
          }
      }
      

      有了这个配置,就可以在项目中使用MP的分页插件了。其中通用Mapper通用Service都提供了分页查询的相关功能,我们以通用Service为例使用MP的分页功能。

      首先我们来看一下分页功能最重要的一个接口IPage,这里直接上源码:

      public interface IPage<T> extends Serializable {
      
          /**
           * 降序字段数组
           *
           * @return order by desc 的字段数组
           * @see #orders()
           */
          @Deprecated
          default String[] descs() {
              return null;
          }
      
          /**
           * 升序字段数组
           *
           * @return order by asc 的字段数组
           * @see #orders()
           */
          @Deprecated
          default String[] ascs() {
              return null;
          }
      
          /**
           * 获取排序信息,排序的字段和正反序
           *
           * @return 排序信息
           */
          List<OrderItem> orders();
      
          /**
           * KEY/VALUE 条件
           *
           * @return ignore
           */
          default Map<Object, Object> condition() {
              return null;
          }
      
          /**
           * 自动优化 COUNT SQL【 默认:true 】
           *
           * @return true 是 / false 否
           */
          default boolean optimizeCountSql() {
              return true;
          }
      
          /**
           * 进行 count 查询 【 默认: true 】
           *
           * @return true 是 / false 否
           */
          default boolean isSearchCount() {
              return true;
          }
      
          /**
           * 计算当前分页偏移量
           */
          default long offset() {
              return getCurrent() > 0 ? (getCurrent() - 1) * getSize() : 0;
          }
      
          /**
           * 当前分页总页数
           */
          default long getPages() {
              if (getSize() == 0) {
                  return 0L;
              }
              long pages = getTotal() / getSize();
              if (getTotal() % getSize() != 0) {
                  pages++;
              }
              return pages;
          }
      
          /**
           * 内部什么也不干
           * 

      只是为了 json 反序列化时不报错

      */
      default IPage<T> setPages(long pages) { // to do nothing return this; } /** * 分页记录列表 * * @return 分页对象记录列表 */ List<T> getRecords(); /** * 设置分页记录列表 */ IPage<T> setRecords(List<T> records); /** * 当前满足条件总行数 * * @return 总条数 */ long getTotal(); /** * 设置当前满足条件总行数 */ IPage<T> setTotal(long total); /** * 当前分页总页数 * * @return 总页数 */ long getSize(); /** * 设置当前分页总页数 */ IPage<T> setSize(long size); /** * 当前页,默认 1 * * @return 当前页 */ long getCurrent(); /** * 设置当前页 */ IPage<T> setCurrent(long current); /** * IPage 的泛型转换 * * @param mapper 转换函数 * @param 转换后的泛型 * @return 转换泛型后的 IPage */ @SuppressWarnings("unchecked") default <R> IPage<R> convert(Function<? super T, ? extends R> mapper) { List<R> collect = this.getRecords().stream().map(mapper).collect(toList()); return ((IPage<R>) this).setRecords(collect); } }

      其中对我们比较重要的属性有current(当前页数)size(每页记录数)pages(总页数)total(总记录数)records(分页数据)

      MP为我们提供了一个简单IPage实现:Page类。下面是一个简单的分页查询例子:

      @Test
      public void testPage() {
          // 设置分页条件
          Page<Player> page = new Page<>();
          page.setCurrent(1);
          page.setSize(5);
          playerService.page(page, Wrappers.<Player>query().eq("team_id", 1L));
          // MP执行的SQL:
          // Preparing: SELECT COUNT(1) FROM player WHERE (team_id = ?)
          // Parameters: 1(Long)
          // Preparing: SELECT id,no,team_id,name FROM player WHERE (team_id = ?) LIMIT ?,?
          // Parameters: 1(Long), 0(Long), 5(Long)
      
          //page.getPages();  // 总页数
          //page.getTotal();  // 总记录数
          // 分页结果保存在 page 的 records 属性中
          Assertions.assertNotEquals(0, page.getRecords().size());
      
          page.setSearchCount(false);    // 是否需要查询总记录数,这里设置不需要
          playerService.page(page, Wrappers.<Player>query().eq("team_id", 1L));
          // MP执行的SQL:
          // Preparing: SELECT id,no,team_id,name FROM player WHERE (team_id = ?) LIMIT ?,?
          // Parameters: 1(Long), 0(Long), 5(Long)
          Assertions.assertNotEquals(0, page.getRecords().size());
      }
      

      这里有一点需要注意,就是当业务对表的总记录数不关心的时候,可以通过Page.setSearchCount(false)方法来避免执行SELECT COUNT(1)操作。

      很多时候我们的分页查询的业务会非常复杂,通过条件构造器也可能不能实现我们的需求,这时候就需要我们自己写SQL了,这不正是MyBatis的强项吗。好在MP对MyBatis原有的特性完全支持,使得我们可以自定义SQL。接下来我们重点讲解MP自定义SQL。

      4.4回到MyBatis - 使用自定义SQL

      在本文开头提到,MP只对MyBatis做了增强,并没有改变MyBatis原有的特性。所以对于原来MyBatis的强项自定义SQL功能,也可以应用到MP中,而且MP也对这部分做了增强。

      提示:本节内容需要MyBatis基础。

      我们之前都是在使用MP来构造我们想要的SQL语句,应对一些很复杂的场景,我们有时可能很难直接通过MP来一步构造SQL语句,这时候我们可以自己写SQL–就像以前用MyBatis一样。

      如果抛开MP只使用MyBatis写SQL的话,是这样的:

      @Select({
          ""
      })
      List find(@Param("team") Team team);
      
      • 注:如果使用XML的方式,在MyBatis框架下还需要配置mybatis.mapper-locations;如果是MP框架,则是配置mybatis-plus.mapper-locations

      上面是原生Mybatis支持的SQL绑定功能(其实MyBatis的优势就体现在这)。虽然这种方式非常灵活,但用过MyBatis做过项目的都知道,因为每一个DAO方法都需要对应一段SQL,再加上各种结果转换和条件判断,SQL充斥着整个项目。MP官方也为我们考虑了这个问题,其在不改变原有方式的情况下做了改进。下面先来介绍下普通操作(CURD)自定义SQL的MP打开方式:

      在Mapper中添加一个名为"ew"(MP规定,可以用Constants.WRAPPER这个值为"ew"的常量)的Wrapper类型参数:

      @Select("SELECT * FROM team ${ew.customSqlSegment}")    // Wrapper.customSqlSegment为自定义SQL的内容
      List<Team> findMP(@Param(Constants.WRAPPER) Wrapper<Team> wrapper);
      

      调用时传入一个条件构造器即可,但需要注意的是,MP的这种自定义SQL的方式只能添加到WHERE条件,但WHERE中的判断逻辑可以省略(如ANDOR的拼接,都交给了Wrapper实现),而且WHERE关键字也不需要我们特地指定。然后通过以下方式调用:

      @Test
      public void testCustomFindMP() {
          List<Team> teamList = teamMapper.findMP(Wrappers.<Team>query().le("id", 5L));
          // MP执行的SQL:
          // Preparing: SELECT * FROM team WHERE (id <= ?)
          // Parameters: 5(Long)
          Assertions.assertNotEquals(0, teamList.size());
      }
      

      分页查询的自定义SQL也非常简单,在Mapper中定义分页查询(Page参数必须是第一个参数):

      @Select("SELECT * FROM player ${ew.customSqlSegment}")
      IPage<Player> pageCustomMP(Page page, @Param(Constants.WRAPPER) Wrapper<Player> wrapper);
      

      调用方式也是我们非常熟悉的方式:

      @Test
      public void testPageCustomMP() {
          IPage<Player> playerIPage = playerMapper.pageCustomMP(new Page(1, 5), Wrappers.<Player>query().le("id", 5L));
          // MP执行的SQL:
          // Preparing: SELECT COUNT(1) FROM player WHERE (id <= ?)
          // Parameters: 5(Long)
          // Preparing: SELECT * FROM player WHERE (id <= ?) LIMIT ?,?
          // Parameters: 5(Long), 0(Long), 5(Long)
          Assertions.assertNotEquals(0, playerIPage.getRecords().size());
      }
      

      5.结尾

      • 本文demo资源:

        • 源码
        • 数据
      • 参考资料:

        • MyBatis-Plus官网
      • 关于MP还有很多更高级的话题,如逻辑删除乐观锁以及分布式事务等,下次再跟大家一起分享。

      你可能感兴趣的:(spring,boot,java,spring,spring,boot,mybatis,mybatis-plus)