MyBatis-Plus是什么?
MyBatis-Plus 是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生
为什么有MyBatis-Plus?
一切的目的都是为了少写甚至不写 Mapper.xml
,通过 Java 代码就能实现数据库的 CRUD
特性
- 无侵入、损耗小、强大的 CURD 操作
- 支持 Lambda 形式调用,支持多种数据库
准备数据库与数据
这里使用的是 MySQL 数据库
1 | CREATE TABLE USER ( |
依赖
一个数据库驱动,版本由 spring-boot-starter-parent 指定
1 | <dependency> |
MyBatis-Plus 的 SpringBoot Starter
1 | <dependency> |
简化 Getter、Setter 的 lombok
1 | <dependency> |
配置
数据库连接
打开 application.yml
,加入以下配置
1 | spring: |
日志
dao 所在的包名是 com.ikutarian.mp.dao
,因此 application.yml
中指定 dao 日志的输出如下,注意需要使用 trace
级别的日志,这样可以看到比 debug
级别更多的日志信息
1 | logging: |
常用注解
- @TableName:表名
- @TableId:表的主键
- @TableField:表的字段名
如果 Java Entity 的属性不是表里的字段,可以这样声明
1 | false) (exist = |
创建 Entity
默认 Entity 的属性是驼峰命名的名称
1 | package com.ikutarian.mp.entity; |
创建 Dao
新建一个接口,继承 BaseMapper
,并且传入 Entity。这样 Dao 接口写好了
1 | package com.ikutarian.mp.dao; |
现在就可以利用 MyBatis-Plus 提供的一系列方法了
新增
1 |
|
控制台输出
1 | DEBUG==> Preparing: INSERT INTO user ( name, age, manager_id, create_time ) VALUES ( ?, ?, ?, ? ) |
查询
打开 BaseMapper
的源码文件,可以看到提供了如下几个查询方法
1 | /** |
根据主键查询
MyBatis-Plus 提供了两个方法:
T selectById(id)
:根据 ID 查询List<T> selectBatchIds(idList)
: 根据 ID 批量查询
比如
1 | /** |
根据字段名与字段值查询
MyBatis-Plus 提供了:
selectByMap(Map<String, Object> columnMap)
比如 SQL 如下
1 | SELECT |
Java 代码是
1 | /** |
条件构造器(Wrapper)查询
具体 API 看官方文档
打开 com.baomidou.mybatisplus.core.conditions.AbstractWrapper
这个类,提供了很多的条件构造方法。为了方便说明,现在已几个需求来进行说明
- 名字中包含“雨”并且年龄小于40
SQL 是
1 | SELECT |
Java 代码
1 |
|
- 名字中包含“雨”并且年龄大于等于20且小于40并且email不为空
SQL是
1 | SELECT |
Java 代码
1 |
|
- 姓王或者年龄大于等于25,按照年龄降序排列,年龄相同按照id升序排列
SQL是
1 | SELECT |
Java 代码
1 |
|
- 创建日期为2019年2月14日,并且直属上级姓王
SQL为
1 | SELECT |
apply
用于调用 SQL 的函数。inSql
用在调用 IN
后面拼接 SQL 语句的场景
1 |
|
注意:使用 apply
的时候推荐使用占位符 {}
,这样可以防止 SQL 注入的风险
- 姓王并且(年龄小于40或者邮箱不为空)
SQL是
1 | SELECT |
这里 AND
后面跟着一个括号,括号里也是一个条件判断。这时候要用 and
Java代码
1 |
|
- 姓王或者(年龄小于40并且大于20并且邮箱不为空)
SQL是
1 | SELECT |
和上面的例子 5 一样,OR
后面也跟着一对括号。这时候可以用 or
Java代码
1 |
|
- (年龄小于40或者邮箱不为空)并且姓王
SQL是
1 | SELECT |
Java代码
和例子 5 和例子 6 不同。这里是正常嵌套,SQL 句子前面不带 AND
或者 OR
,这时候要借助 nested
1 |
|
- 年龄为 30 或者 31 或者 34 或者 35
SQL是
1 | SELECT |
Java代码
1 |
|
- 年龄为30、31、34、35,只返回满足条件的其中一条语句即可
SQL是
1 | SELECT |
Java代码
1 |
|
注意:last()
方法只能调用 1 次
SELECT不列出全部字段
默认是查询所有字段,如果只需要特定的字段,要怎么做呢?,可以利用 select 来实现
- 名字中包含“雨”并且年龄小于40,只列出 id 和 name 字段
SQL
1 | SELECT |
Java代码
1 |
|
- 名字中包含“雨”并且年龄小于40,除了 create_time 和 manager_id 字段外都列出
SQL
1 | SELECT |
Java代码
除了 create_time 和 manager_id 字段外都列出,select
也支持
1 |
|
condition的作用
有一些方法可以传入 condition 的参数,它有什么作用?举个例子来说明
比如这样的场景,如果 name 不为空才进行查询,email 不为空才进行查询
1 |
|
因为 name 不为空,email 为空,根据代码
1 | if (StringUtils.isNotEmpty(name)) { |
拼接得到的 SQL 为
1 | SELECT * FROM user WHERE name LIKE '%王%' |
虽然需求实现了,但是代码不够优雅。这时候,可以利用 3 个参数的 QueryWrapper 的方法进行 SQL 拼接
1 |
|
原理就是:
只有第一个参数
condition
成立时才进行 SQL 拼接
创建条件构造器时传入实体对象
可以传一个实体对象,MyBatis-Plus 会根据实体对象的属性去创造 SQL。这个比 selectByMap(Map<String, Object> columnMap)
方法可读性更好
1 |
|
控制台输入日志为
1 | DEBUG==> Preparing: SELECT id,name,age,email,manager_id,create_time FROM user WHERE name=? AND age=? |
allEq的使用
allEq 需要传入一个 Map
。allEq
方法还可以接受第二个 boolean 类型的参数 null2IsNull
。它表示如果传入的 KV 键值对中 V 是 null
的话,就转换成 K IS NULL
比如,这样的 SQL 语句
1 | SELECT |
使用 allEq
实现的话,Java 代码如下
1 |
|
如果传入的 null2IsNull
为 false
,那么 KV 键值对中的 V 为 null
的话,就会被忽略
1 |
|
日志打印的 SQL 语句是
1 | DEBUG==> Preparing: SELECT id,name,age,email,manager_id,create_time FROM user WHERE name = ? AND age = ? |
可以看到 email
- null
键值对被忽略了
将查询结果以 List<Map<String, Object>> 的形式返回
调用 BaseMapper
的 selectMaps
方法来实现
1 | /** |
比如这个例子
1 |
|
有这样一个需求:按照直属上级分组,查询每组的平均年龄、最大年龄、最小年龄,并且取总年龄小于 500 的组。就可以利用 BaseMapper.selectMaps
取到结果,而不用去重新创建一个实体类来包装结果
SQL是
1 | SELECT |
Java代码
1 |
|
日志输出
1 | DEBUG==> Preparing: SELECT AVG(age) AS avg_age,MIN(age) AS min_age,MAX(age) AS max_age FROM user GROUP BY manager_id HAVING SUM(age) < ? |
只返回第一个字段的值
1 | /** |
比如,虽然使用 select
指定要返回 id
和 name
,但数据只有 id
的数据
1 |
|
观察SQL日志,发现还是有查询 name
和 age
,只不过只把第一个字段 id
的值取出返回
1 | DEBUG==> Preparing: SELECT id,name FROM user WHERE name LIKE ? AND age < ? |
获取符合条件的记录数
可以利用 selectCount
实现
1 | /** |
一个例子
1 |
|
SQL日志是
1 | DEBUG==> Preparing: SELECT COUNT( 1 ) FROM user WHERE name LIKE ? AND age < ? |
根据查询条件,只返回一条记录
利用 selectOne
实现
1 | /** |
有个需要注意的地方:如果查询的结果不止 1 条就会抛出一个 org.apache.ibatis.exceptions.TooManyResultsException
异常。只有 0 条和 1 条时是正常的
比如
1 |
|
自定义SQL查询时,使用 QueryWrapper
在自定义 SQL 时,也可以利用到 QueryWrapper 的便利性。有两种方式来实现:
- 注解方式 Mapper.java
1 | "select * from mysql_data ${ew.customSqlSegment}") ( |
- XML形式 Mapper.xml
1 | <select id="getAll" resultType="MysqlData"> |
具体可以看文档
第一种方式:注解方式 Mapper.java
1 | package com.ikutarian.mp.dao; |
调用时
1 |
|
SQL日志
1 | DEBUG==> Preparing: SELECT * FROM user WHERE name LIKE ? AND age < ? |
第二种方式:XML形式 Mapper.xml
现在 SQL 语句不用注解了
1 | package com.ikutarian.mp.dao; |
而是写在 user.xml 里
1 |
|
使用还是一样
1 |
|
SQL日志输出也一样
1 | DEBUG==> Preparing: SELECT * FROM user WHERE name LIKE ? AND age < ? |
分页查询
要实现分页查询,需要两个步骤:
- 配置分页插件
- 调用
BaseMapper
的selectPage
方法
配置分页插件
1 | package com.ikutarian.mp.config; |
调用 BaseMapper 的 selectPage 方法
1 | /** |
方法需要传入两个参数:
IPage<T>
Wrapper<T>
IPage<T>
可以使用实现类 Page<T>
,Wrapper<T>
跟上面的一样使用 QueryWrapper<T>
即可
举个例子
1 |
|
SQL日志是
1 | DEBUG==> Preparing: SELECT COUNT(1) FROM user WHERE age >= ? |
可以看到查询了两次,第一次先查询总记录数
1 | DEBUG==> Preparing: SELECT COUNT(1) FROM user WHERE age >= ? |
如果总记录数大于 0,再进行分页查询
1 | DEBUG==> Preparing: SELECT id,name,age,email,manager_id,create_time FROM user WHERE age >= ? LIMIT ?,? |
分页查询返回 List<Map<String, Object>> 结果
分页查询不止有 selectPage
方法,还有 selectMapsPage
方法。只不过 selectMapsPage
的返回值类型是 IPage<Map<String, Object>>
使用上面的例子,这次调用 selectMapsPage
方法
1 |
|
SQL日志是
1 | DEBUG==> Preparing: SELECT COUNT(1) FROM user WHERE age >= ? |
分页查询,但是不需要总记录数
如果要返回总记录数的话,分页查询需要发起 2 次查询。但是有些场景不需要返回总记录数,这时候再进行 2 次查询就是浪费资源了。这时候可以利用 Page<T>
的另外一个构造方法
1 | public Page(long current, long size, boolean isSearchCount) { |
第三个参数 isSearchCount
表示是否进行 count 查询,传入 false
就不会进行 count 查询了
1 |
|
SQL日志是
1 | DEBUG==> Preparing: SELECT id,name,age,email,manager_id,create_time FROM user WHERE age >= ? LIMIT ?,? |
现在只有 1 次查询了
自定义SQL的分页查询
有两个要求:
- 返回值必须是
IPage<T>
- Mapper 接口的方法的第一个参数必须是
Page<T>
举个例子
user.xml
1 |
|
Java代码
1 |
|
SQL日志
1 | DEBUG==> Parameters: |
lambda 条件构造器查询
上面使用 QueryWrapper
和 UpdateWrapper
的时候,都需要手工填写字段名。如果表结构改了,那么就要去手工查找与替换字段名。如果用 lambda 的条件构造器的话,就可以省略很多工作量了
现在使用 lambda 条件构造器重写例子 1 的代码。名字中包含“雨”并且年龄小于40,SQL 是
1 | SELECT |
Java 代码
1 |
|
更新
MyBatis-Plus 提供了两个关于更新的方法
1 | /** |
根据ID更新
Java代码
1 |
|
SQL日志
1 | DEBUG==> Preparing: UPDATE user SET age=?, email=? WHERE id=? |
以条件构造器为参数的更新
需要传入两个参数:
- 实体
- 条件构造器:
UpdateWrapper<T>
实体就是 SQL 语句中的 SET xx = xx
的内容,条件构造器就是 SQL 中 where xxx
的部分
比如这样一条 SQL
1 | UPDATE |
用 Java 代码实现就是
1 |
|
SQL 日志是
1 | DEBUG==> Preparing: UPDATE user SET age=?, email=? WHERE name = ? AND age = ? |
以条件构造器为参数的更新 2.0
还有一种方法可以省略实体,直接使用 UpdateWrapper<T>
设置要更新的值
还是上面的例子,不过这次不需要传入实体,直接调用 UpdateWrapper<T>
的 set
方法来设置新的值
1 |
|
SQL日志
1 | DEBUG==> Preparing: UPDATE user SET email=?,age=? WHERE name = ? AND age = ? |
删除
MyBatis-Plus 提供了以下几个删除的 API
1 | /** |
根据ID删除
传入主键即可
1 |
|
根据ID批量删除
1 |
|
根据columnMap条件,删除记录
columnMap
中存储的 KV 键值对,就是 SQL 中对应的 WHERE k = v
部分
1 |
|
使用条件构造器进行删除
使用 QueryWrapper<T>
来实现 DELETE SQL 语句中 WHERE xx = xx
的部分
1 |
|
SQL日志是
1 | DEBUG==> Preparing: DELETE FROM user WHERE name = ? AND age = ? |