🐦MyBatis-Plus
[TOC]
🚪 1. 快速入门
1.1 入门案例
入门案例:基于课前资料提供的项目,实现下列功能:
- 新增用户功能
- 根据
id
查询用户
- 根据
id
批量查询用户
- 根据
id
更新用户
- 根据
id
删除用户
1.1.1 引入 MyBatis-Plus
的依赖
1 2 3 4 5 6
| <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency>
|
1.1.2 定义 Mapper
自定义的 Mapper
继承 MybatisPlus
提供的 BaseMapper
接口。
BaseMapper<T>
给我们提供了很多基础方法,如常用的增、删、改、查等操作。
1
| public interface UserMapper extends BaseMapper<User> { }
|
1.1.3 直接使用
做完上面两步操作,此时我们不需要再编写复杂的 SQL
语句即可对数据库进行增、删、改、查操作。
当然对于原有 MyBatis
操作,我们也可以直接使用,因为 MyBatis-Plus
对 MyBatis
是非侵入的,体现了其润物无声的特点。
1.2 常见注解
在初次使用 MyBatis-Plus
时,我们并没有指定要执行操作的表信息和字段信息,那么 Mybatis-Plus
是如何知道我们要查询的是哪张表?表中有哪些字段呢?
MyBatisPlus
通过扫描实体类,并基于反射获取实体类信息作为数据库表信息。
在入门案例中 1.1.2
定义 Mapper
时,我们需要特别指定泛型类即 <User>
。泛型中的 User
就是与数据库对应的 PO。
MyBatis-Plus
就是根据 PO 实体的信息来推断出表的信息,从而生成 SQL
的。默认情况下:
MyBatis-Plus
会把 PO 实体的类名驼峰转下划线作为表名,如类名为 UserInfo
,反射的表名为 user_info
;
MyBatis-Plus
会把 PO 实体的所有变量名驼峰转下划线作为表的字段名,并根据变量类型推断字段类型;
MyBatis-Plus
会把名为 id
的字段作为主键。
但很多情况下,默认的实现与实际场景不符,因此 MyBatis-Plus
提供了一些注解便于我们声明表信息。
1.2.1 @TableName
注解
- 描述:表名注解,标识实体类对应的表
- 使用位置:实体类
1 2 3 4 5 6
| @TableName("t_user") public class User { private Long id; private String name; }
|
@TableName
注解除了指定表名以外,还可以指定很多其它属性:
属性 |
类型 |
必须指定 |
默认值 |
描述 |
value |
String |
否 |
“” |
表名 |
schema |
String |
否 |
“” |
schema |
keepGlobalPrefix |
boolean |
否 |
false |
是否保持使用全局的 tablePrefix 的值(当全局 tablePrefix 生效时) |
resultMap |
String |
否 |
“” |
xml 中 resultMap 的 id(用于满足特定类型的实体类对象绑定) |
autoResultMap |
boolean |
否 |
false |
是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建与注入) |
excludeProperty |
String[] |
否 |
{} |
需要排除的属性名 @since 3.3.1 |
1.2.2 @TableId
注解
- 描述:主键注解,标识实体类中的主键字段
- 使用位置:实体类的主键字段
1 2 3 4 5 6
| @TableName("t_user") public class User { @TableId(value="id", type=IdType.AUTO) private Long id; private String name; }
|
TableId
注解支持两个属性:
属性 |
类型 |
必须指定 |
默认值 |
描述 |
value |
String |
否 |
“” |
表名 |
type |
Enum |
否 |
IdType.NONE |
指定主键类型 |
IdType
支持的类型有:
值 |
描述 |
AUTO |
数据库 ID 自增 |
NONE |
无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT ) |
INPUT |
insert 前自行 set 主键值 |
ASSIGN_ID |
分配 ID (主键类型为 Number (Long 和 Integer )或(String )(since 3.3.0),使用接口IdentifierGenerator 的方法 nextId (默认实现类为 DefaultIdentifierGenerator 雪花算法) |
ASSIGN_UUID |
分配 UUID ,主键类型为 String *(since 3.3.0)*,使用接口 IdentifierGenerator 的方法 nextUUID (默认 default 方法) |
ID_WORKER |
分布式全局唯一 ID 长整型类型(please use ASSIGN_ID) |
UUID |
32 位 UUID 字符串(please use ASSIGN_UUID) |
ID_WORKER_STR |
分布式全局唯一 ID 字符串类型(please use ASSIGN_ID) |
这里比较常见的有三种:
AUTO
:利用数据库的 id
自增长
INPUT
:手动生成 id
ASSIGN_ID
:雪花算法生成 Long
类型的全局唯一 id
,这是默认的ID策略
1.2.3 @TableField
注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @TableName("user") public class User { @TableId(value="u_id", type=IdType.AUTO) private Long id; @TableField("username") private String name; private Integer age; @TableField("isMarried") private Boolean isMarried; @TableField("`concat`") private String concat;
@TableField(exist = false) private String address; }
|
一般情况下我们并不需要给字段添加 @TableField
注解,一些特殊情况除外:
- 成员变量名与数据库字段名不一致;
- 成员变量是以
isXXX
命名,按照JavaBean
的规范,MyBatis-Plus
识别字段时会把 is
去除,这就导致与数据库不符;
- 成员变量名与数据库一致,但是与数据库的关键字冲突。使用
@TableField
注解给字段名添加转义字符:``
;
- 成员变量不是数据库字段,添加
exist = false
支持的其它属性如下:
属性 |
类型 |
必填 |
默认值 |
描述 |
value |
String |
否 |
“” |
数据库字段名 |
exist |
boolean |
否 |
true |
是否为数据库表字段 |
condition |
String |
否 |
“” |
字段 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 %s=#{%s} ,参考(opens new window) |
update |
String |
否 |
“” |
字段 update set 部分注入,例如:当在version字段上注解update="%s+1" 表示更新时会 set version=version+1 (该属性优先级高于 el 属性) |
insertStrategy |
Enum |
否 |
FieldStrategy.DEFAULT |
举例:NOT_NULL insert into table_a(<if test="columnProperty != null">column</if>) values (<if test="columnProperty != null">#{columnProperty}</if>) |
updateStrategy |
Enum |
否 |
FieldStrategy.DEFAULT |
举例:IGNORED update table_a set column=#{columnProperty} |
whereStrategy |
Enum |
否 |
FieldStrategy.DEFAULT |
举例:NOT_EMPTY where <if test="columnProperty != null and columnProperty!=''">column=#{columnProperty}</if> |
fill |
Enum |
否 |
FieldFill.DEFAULT |
字段自动填充策略 |
select |
boolean |
否 |
true |
是否进行 SELECT 查询 |
keepGlobalFormat |
boolean |
否 |
false |
是否保持使用全局的 format 进行处理 |
jdbcType |
JdbcType |
否 |
JdbcType.UNDEFINED |
JDBC 类型 (该默认值不代表会按照该值生效) |
typeHandler |
TypeHander |
否 |
|
类型处理器 (该默认值不代表会按照该值生效) |
numericScale |
String |
否 |
“” |
指定小数点后保留的位数 |
1.3 常见配置
MyBatis-Plus
也支持基于 yaml
文件的自定义配置,详见官方文档:
MyBatis-Plus 官方文档
大多数的配置都有默认值,因此我们都无需配置。但还有一些是没有默认值的,例如:
1 2 3 4 5 6 7 8 9 10
| mybatis-plus: type-aliases-package: com.itheima.mp.domain.po mapper-locations: "classpath*:/mapper/**/*.xml" configuration: mapper-underscore-to-camel-case: true cache-enabled: false global-config: db-config: id-type: auto update-strategy: not_null
|
需要注意的是,MyBatis-Plus
也支持手写 SQL
的,而 mapper
文件的读取地址可以通过 mapper-locations
配置:默认值是classpath*:/mapper/**/*.xml
,也就是说我们只要把 mapper.xml
文件放置这个目录下就一定会被加载。
🥑 2. 核心功能
入门案例中都是以 id
为条件的简单CRUD,一些复杂条件的 SQL
语句就要用到一些更高级的功能了。
2.1 条件构造器
除了新增以外,修改、删除、查询的 SQL
语句都需要指定 WHERE
条件。因此 BaseMapper
中提供的相关方法除了以 id
作为 WHERE
条件以外,还支持更加复杂的 WHERE
条件。
参数中的 Wrapper
就是条件构造的抽象类,其下有很多默认实现,继承关系如图:
Wrapper
的子类 AbstractWrapper
提供了 WHERE
中包含的所有条件构造方法:
而 QueryWrapper
在 AbstractWrapper
的基础上拓展了一个 SELETE
方法,允许指定查询字段:
而 UpdateWrapper
在 AbstractWrapper
的基础上拓展了一个 SET
方法,允许指定 SQL
中的 SET
部分:
接下来,我们就来看看如何利用 Wrapper
实现复杂查询。
2.1.1 QueryWrapper
无论是修改、删除、查询,都可以使用QueryWrapper来构建查询条件。
当前表的结构如下:
# |
名称 |
数据类型 |
注释 |
长度/集合 |
默认 |
1 |
id |
BIGINT |
用户ID |
19 |
AUTO_INCREMENT |
2 |
username |
VARCHAR |
用户名 |
50 |
无默认值 |
3 |
password |
VARCHAR |
密码 |
128 |
无默认值 |
4 |
phone |
VARCHAR |
注册手机号 |
20 |
NULL |
5 |
info |
JSON |
详细信息 |
|
无默认值 |
6 |
status |
INT |
使用状态(1: 正常 2: 冻结) |
10 |
“1” |
7 |
balance |
INT |
账户余额 |
10 |
|
8 |
create_time |
DATETIME |
创建时间 |
|
CURRENT_TIMESTAMP |
9 |
update_time |
DATETIME |
更新时间 |
|
CURRENT_TIMESTAMP |
接下来看一些例子:
查询:查询出名字中带o
的,存款大于等于1000元的人。代码如下:
1 2 3
| SELECT id,username,info,balance FROM user WHERE username LIKE "%o%" AND Balance >= 100
|
1 2 3 4 5 6 7 8 9 10 11
| @Test void testQueryWrapper() { QueryWrapper<User> wrapper = new QueryWrapper<User>() .select("id", "username", "info", "balance") .like("username", "o") .ge("balance", 1000); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); }
|
更新:更新用户名为 Jack
的用户的余额为 2000
,代码如下:
1 2 3
| UPDATE user SET balance=2000 WHERE (username="Jack")
|
1 2 3 4 5 6 7 8 9 10
| @Test void testUpdateByQueryWrapper() { QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("username", "Jack"); User user = new User(); user.setBalance(2000); userMapper.update(user, wrapper); }
|
2.1.2 UpdateWrapper
基于 BaseMapper
中的 UPDATE
方法更新时只能直接赋值,对于一些复杂的需求就难以实现。
例如:更新 id
为 1
、2
、4
的用户的余额,扣 200
,对应的SQL应该是:
1
| UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4)
|
SET
的赋值结果是基于字段现有值的,这个时候就要利用 UpdateWrapper
中的 setSql
功能了:
1 2 3 4 5 6 7 8 9 10 11 12
| @Test void testUpdateWrapper() { List<Long> ids = List.of(1L, 2L, 4L); UpdateWrapper<User> wrapper = new UpdateWrapper<User>() .setSql("balance = balance - 200") .in("id", ids); userMapper.update(null, wrapper); }
|
2.1.3 LambdaQueryWrapper
无论是 QueryWrapper
还是 UpdateWrapper
在构造条件的时候都需要写死字段名称,会出现字符串魔法值
。这在编程规范中显然是不推荐的。 那怎么样才能不写字段名,又能知道字段名呢?
其中一种办法是基于变量的 getter
方法结合反射技术。因此我们只要将条件对应的字段的 getter
方法传递给 MyBatis-Plus
,它就能计算出对应的变量名了。而传递方法可以使用 JDK8
中的 方法引用
和 Lambda
表达式。 因此 MyBatis-Plus
又提供了一套基于Lambda
的 Wrapper
,包含两个:
LambdaQueryWrapper
LambdaUpdateWrapper
分别对应 QueryWrapper
和 UpdateWrapper
其使用方式如下:
1 2 3 4 5 6 7 8 9 10 11 12
| @Test void testLambdaQueryWrapper() { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.lambda() .select(User::getId, User::getUsername, User::getInfo, User::getBalance) .like(User::getUsername, "o") .ge(User::getBalance, 1000); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); }
|
2.2 自定义 SQL
一般企业的开发规范中,是不允许开发人员将 SQL
脱离出 Mapper
层或者 mapper.xml
文件的,但是我们想要使用 MyBatis-Plus
又想要遵守企业的开发规范,应该如何做呢?
我们可以利用 MyBatis-Plus
的 Wrapper
来构建复杂的 WHERE
条件,然后自己定义 SQL
语句中剩下的部分。
① 基于 Wrapper
构造 WHERE
条件
1 2 3 4 5 6 7 8
| List<Long> ids = List.of(1L, 2L, 4L); int amount = 200;
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().in(User::getId, ids);
userMapper.updateBalanceByIds(wrapper, amount);
|
② 在 mapper
方法参数中用 @Param
注解声明 Wrapper
变量名称,这个名称必须是 ew
1
| void updateBalanceByIds(@Param("ew") LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);
|
③ 自定义 SQL
,并使用 Wrapper
条件,这里 ew.customSqlSegment
是 Wrapper
的一个方法,用于获取用户自定义SQL片段。如果使用出现问题,参考博客:MyBatis-Plus ${ew.customSqlSegment} 使用的史诗级大坑-CSDN博客
1 2 3
| <update id="updateBalanceByIds"> UPDATE t_user SET balance = balance - #{amount} ${ew.customSqlSegment}; </update>
|
2.3 Service
接口
MyBatis-Plus
不仅提供了 BaseMapper
,还提供了通用的 Service
接口及默认实现,封装了一些常用的 service
模板方法。 通用接口为 IService
,默认实现为 ServiceImpl
,其中封装的方法可以分为以下几类:
save
:新增
remove
:删除
update
:更新
get
:查询单个结果
list
:查询集合结果
count
:计数
page
:分页查询
2.3.1 CRUD
首先了解基本的CRUD接口。
新增:
save
是新增单个元素
saveBatch
是批量新增
saveOrUpdate
是根据id判断,如果数据存在就更新,不存在则新增
saveOrUpdateBatch
是批量的新增或修改
删除:
removeById
:根据id删除
removeByIds
:根据id批量删除
removeByMap
:根据Map中的键值对为条件删除
remove(Wrapper<T>)
:根据Wrapper条件删除
~~removeBatchByIds~~
:暂不支持
修改:
updateById
:根据id修改
update(Wrapper<T>)
:根据UpdateWrapper
修改,Wrapper
中包含set
和where
部分
update(T,Wrapper<T>)
:按照T
内的数据修改与Wrapper
匹配到的数据
updateBatchById
:根据id批量修改
Get:
getById
:根据 id
查询 1
条数据
getOne(Wrapper<T>)
:根据Wrapper
查询 1
条数据
getBaseMapper
:获取Service
内的 BaseMapper
实现,某些时候需要直接调用 Mapper
内的自定义 SQL
时可以用这个方法获取到 Mapper
List:
listByIds
:根据id批量查询
list(Wrapper<T>)
:根据Wrapper条件查询多条数据
list()
:查询所有
Count:
count()
:统计所有数量
count(Wrapper<T>)
:统计符合Wrapper
条件的数据数量
getBaseMapper: 当我们在service中要调用Mapper中自定义SQL时,就必须获取service对应的Mapper,就可以通过这个方法:
2.3.2 基本用法
由于 Service
中经常需要定义与业务有关的自定义方法,因此我们不能直接使用 IService
,而是自定义 Service
接口,然后继承 IService
以拓展方法。同时,让自定义的 Service实现类
继承 ServiceImpl
,这样就不用自己实现 IService
中的接口了。
具体方法如下:
① 首先,定义 IUserService
类,继承 IService
:
1 2 3 4 5 6 7 8
| package com.itheima.mp.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.itheima.mp.domain.po.User;
public interface IUserService extends IService<User> { }
|
② 然后,编写 UserServiceImpl
类,继承 ServiceImpl
,实现 UserService
:
1 2 3 4 5 6 7 8 9 10 11 12
| package com.itheima.mp.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.itheima.mp.domain.po.User; import com.itheima.mp.domain.po.service.IUserService; import com.itheima.mp.mapper.UserMapper; import org.springframework.stereotype.Service;
@Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
}
|
项目结构如下:
接下来,我们基于 RESTful
风格快速实现下面几个接口(什么是RESTful
风格):
简单理解 REST
就是:URL
中只使用名词来定位资源,用 HTTP
协议里的动词(GET
、POST
、PUT
、DELETE
)来实现资源的增删改查操作。
什么是 RESTful
:
- 使用客户/服务器(B/S、 C/S)模型:客户和服务器之间通过一个统一的接口来互相通讯。
- 层次化的系统:在一个
REST
系统中,客户端并不会固定地与一个服务器打交道。
- 无状态:在一个
REST
系统中,服务端并不会保存有关客户的任何状态。也就是说,客户端自身负责用户状态的维持,并在每次发送请求时都需要提供足够的信息。
- 可缓存:
REST
系统需要能够恰当地缓存请求,以尽量减少服务端和客户端之间的信息传输,以提高性能。
- 统一的接口。一个
REST
系统需要使用一个统一的接口来完成子系统之间以及服务与用户之间的交互。这使得 REST
系统中的各个子系统可以独自完成演化。
如果一个系统满足了上面所列出的五条约束,那么该系统就被称为是 RESTful
的。
编号 |
接口 |
请求方式 |
请求路径 |
请求参数 |
返回值 |
1 |
新增用户 |
POST |
/users |
用户表单实体 |
无 |
2 |
删除用户 |
DELETE |
/users/{id} |
用户 id |
无 |
3 |
根据 id 查询用户 |
GET |
/users/{id} |
用户 id |
用户VO |
4 |
根据 id 批量查询 |
GET |
/users |
用户 id 集合 |
用户VO集合 |
5 |
根据 id 扣除余额 |
PUT |
/users/{id}/deduction/{money} |
1. 用户 id 2. 扣除金额 |
无 |
🕸 3. 拓展功能
🧩 4. 插件功能