在Spring框架风靡java开发的今天,java应用的持久层框架主要有MyBatis
和JPA(Spring Data JPA)
。熟悉这两个框架的开发者很容易就能总结出以下结论:
MyBatis优势
JPA优势
对比这两个框架的优势,也可以很容易发现MyBatis
的劣势:
那么,有没有一种框架可以结合这两个框架的优势,使得我们开发应用的效率可以大大提高,又可以灵活、好维护呢?答案正是我们今天要讨论的主角:MyBatis-Plus
。
MyBatis-Plus
荣获【2018年度开源中国最受欢迎的中国软件】 TOP5 :
本文将以一个简单的例子来讲解MyBatis-Plus在项目开发中的应用。
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
MyBatis-Plus官方的愿景是成为 MyBatis 最好的搭档,就像 魂斗罗 中的 1P、2P,基友搭配,效率翻倍。
以下是MP的框架结构:
在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 要求如下:
connector
以及数据源spring.datasource
的配置也是必不可少的。在正式进入实战之前,我们先设计一套用来操作的数据库:
现有一张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
框架,可以省去定义pojo
的getter
和setter
的繁琐工作,代码也更简洁。为了可以看到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)**的源码:
可以看到通用Mapper提供了很多应对各种业务需求的数据库操作,现在我们先创建之前提到的两个DO
类对应的DAO
操作类:
public interface TeamMapper extends BaseMapper<Team> {
}
public interface PlayerMapper extends BaseMapper<Player> {
}
非常简单,我们现在一行代码都不需要。接下来我们还需要在Spring Boot
启动类中使用@MapperScan
配置Mapper
包扫描。下面我们对Mapper
的操作进行一一介绍。
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
这一列是一长串数字(当然你运行的结果跟我的很大概率是不一样的数字),即使你设置了表主键是自增长策略,依然是这个情况。
这是因为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)
注解来设置非数据库表字段。Select
相关方法:
T selectById(Serializable id);
// 根据 ID 查询List selectBatchIds(@Param(Constants.COLLECTION) Collection extends Serializable> 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
// 根据 Wrapper 条件,查询全部记录List
// 根据 Wrapper 条件,查询全部记录。注意: 只返回第一个字段的值IPage selectPage(IPage page, @Param(Constants.WRAPPER) Wrapper queryWrapper);
// 根据 entity 条件,查询全部记录(并翻页)IPage
// 根据 Wrapper 条件,查询全部记录(并翻页)MP中的Select
和Delete
都提供了多种操作,总的来说可以分成根据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关于条件构造器主要涉及到AbstractWrapper
、QueryWrapper(LambdaQueryWrapper)
和UpdateWrapper(LambdaUpdateWrapper)
。
AbstractWrapper
QueryWrapper(LambdaQueryWrapper)
和UpdateWrapper(LambdaUpdateWrapper)
的父类,用于生成sql
的where
条件,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
还提供了selectOne
、selectMaps
等方法,大家可以自行设计应用场景来熟悉我们没有使用到的方法,调用方法都类似。
另外,这里再介绍一个更优雅的查询方式,使用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
使用了=
,如果我们希望name
以LIKE
的形式来构建查询,那么我们可以在name
属性上设置@TableField(condition = SqlCondition.LIKE)
,这时单元测试就跟我们预想的一样了。
Update
相关方法:
updateById
// 根据 ID 修改update
// 根据 whereEntity 条件,更新记录MP的Update
操作都是基于entity
,entity
的非空属性将会成为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
值了。
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 extends Serializable> idList);
// 删除(根据ID 批量删除)Delete
操作涉及的方法相对简单,直接通过Id
或者构建WHERE
即可。
MP的另一大特色是框架为我们提供了通用Service,使得我们只需关注系统业务。
说明:
- 通用 Service CRUD 封装IService接口,进一步封装 CRUD 采用
get 查询单行
、remove 删除
、list 查询集合
、page 分页
前缀命名方式区分Mapper
层避免混淆。- 泛型
T
为任意实体对象- 建议如果存在自定义通用 Service 方法的可能,请创建自己的
IBaseService
继承Mybatis-Plus
提供的基类- 对象
Wrapper
为 条件构造器
通用Service的操作跟通用Mapper类似,区别是按照Service
层规范把DAO
层的insert
、select
、delete
改成了save
、get\list
和remove
。
现在我们也创建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操作方式基本保持一致,需要注意的是,通用Service的save
和update
操作返回值是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)
}
在实际项目中用得最多也比较重要的业务应该就是分页操作了,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。
在本文开头提到,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
中的判断逻辑可以省略(如AND
或OR
的拼接,都交给了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());
}
本文demo资源:
参考资料:
关于MP还有很多更高级的话题,如逻辑删除,乐观锁以及分布式事务等,下次再跟大家一起分享。