2026.3.11MyBatis-Plus基本使用与思考
快速入门
入门案例
1.引入MybatisPlus起步依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
MyBatisPlus官方提供了starter,其中集成了Mybatis和MybatisPlus的所有功能,并且实现了自动装配效果。因此我们可以使用该依赖代替Mybatis的starter
2.定义Mapper
我们可以使用自定义的Mapper继承MybatisPlus提供的BaseMapper接口,例如:
public interface UserMapper extends BaseMapper<User>{
}
BaseMapper中已经提前定义好了大量增删改查的方法,如下:

我们需要的简单的增删改查方法都已经在接口中提前定义好了,如此便可省去我们在Mapper中自行写方法并创建xml文件的麻烦
可见,我们比使用mybatis更快地实现了以下五个增删改查语句:
-
新增用户功能
-
根据id查询用户
-
根据id批量查询用户
-
根据id更新用户
-
根据id删除用户
注解
MyBatisPlus通过扫描实体类,并基于反射获取实体类信息作为数据库表信息
反射在哪?如下图所示

但是实体类里面有这么多信息,我怎么知道看哪些信息呢?下面是MyBatisPlus定义的一些规则
-
类名驼峰转下划线作为表名,如果我们这个类叫UserInfo,转下划线就会变成user_info
-
名为id的字段会作为主键
-
变量名驼峰转下划线作为表的字段名,例如createTime会转为create_time
如果我们的实体类不符合这些规定,该怎么办呢?因此我们需要一些注解来帮助我们自定义表名 、主键名 、字段名
常见注解
-
@TableName:用来指定表名
-
@TableId:用来指定表中的主键字段信息
-
@TableField:用来指定表中普通字段的信息
例如IDEA中还是上述的User实体类,而我们此时想让其指定的表名为tb_user,此时我们就要用到@TableName的注解。
id字段同理,但id字段有特殊的类型,我们可以在@TableId中用type属性修饰
-
IdType枚举
-
AUTO:数据库自增长
-
INPUT:通过set方法自行输入
-
ASSIGN_ID:分配ID,默认实现类 com. baomidou. mybatisplus. core. incrementer. DefaultIdentifierGenerator(雪花算法)。
-
成员变量同理,但也有特殊用法
-
使用@TableField常见场景
-
成员变量名与数据库字段名不一致
-
成员变量名以is开头,且是布尔值。因为MP在底层会进行反射的机制,获取字段名称,经过反射处理会去掉原字段的is,并把剩下的字符作为字段名访问数据库。
-
成员变量名与数据库关键字冲突,如order
-
成员变量根本不是数据库字段
-
@TableName("tb_user")
public class User{
//最好指定type类型,如果不指定,默认用雪花算法
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField("username")
private String name;
@TableField("is_married")
private Boolean isMarried;
//用转义字符
@TableField("`order`")
private Integer order;
//如果address不是数据库字段,我们需要将其忽略,以免MP底层在反射过程中报错
@TableField(exist = false)
private String address;
}
常见配置
-
导入MP的类型别名的包
-
自定义全局配置,主要是id-type,但一般而言,注解中的配置比全局配置优先级更高
总结
MP的使用基本流程如下:
-
引入起步依赖
-
自定义Mapper基础BaseMapper
-
在实体类上添加注解声明 表信息
-
在application.yml中根据需要添加配置
核心功能
条件构造器
MP支持各种复杂的Where条件,可以满足日常开发的所有需求
以下为BaseMapper中特殊一点的方法

以上的Wrapper称为"条件构造器",专门用以构造复杂SQL语句,以下为其类图


AbstractWrapper的子类QueryWrapper在父类的基础上做了select功能的拓展

UpdateWrapper做了set功能的拓展

AbstractLambdaWrapper的功能与前面几个近似,不过加入了lambda的语法
例题:
QueryWrapper
查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段
SELECT id,username,info,balance
FROM user
WHERE username LIKE ? AND balance >= ?
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
UPDATE user
SET balance = 2000
WHERE (username = "jack")
//1.要更新的数据
User user = new User();
user.setBalance(2000);
//2.更新的条件
QueryWrapper wrapper = new QueryWrapper<User>().eq("username","jack");
//3.执行更新
userMapper.update(user,wrapper);
UpdateWrapper
更新id为1,2,4的用户的余额,扣200
-- 该题set中的数字不能写死,因此在Java中考虑不能使用UpdateWrapper
UPDATE user
SET balance = balance - 200
WHERE id in (1,2,4)
List<Long> ids = List.of(1L,2L,4L);
UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
.setSql("balance = balance - 200") //使用UpdateWrapper字符串拼接方法
.in("id",ids);
//这里不需要在实体类中写死,因此传入null
userMapper.update(null,wrapper);
lambda语法
查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段
SELECT id,username,info,balance
FROM user
WHERE username LIKE ? AND balance >= ?
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
.select(User::getId,User::getUsername,User::getInfo,User::getBalance) //lambda表达式利用反射得到对应的字段名,减少字符串硬编码的情况
.like(User::getUsername,"o")
.ge(User::getBalance,1000);
List<User> users = userMapper.selectList(wrapper);
users.foreach(System.out::println);
总结
-
QueryWrapper和LambdaQueryWrapper通常用来构建select、delete、update的where条件部分
-
UpdateWrapper和LambdaUpdateWrapper通常只有在set语句比较特殊时才使用
-
尽量使用LambdaQueryWrapper和LambdaUpdateWrapper,避免硬编码
自定义SQL
我们可以利用MP的Wrapper来构建复杂的Where条件,然后自己定义SQL语句中剩下的部分。
需求
将id在指定范围的用户(例如1、2、4)的余额扣减指定值
UPDATE user
SET balance = balance - 200
WHERE id in (1,2,4)
回顾前面的UpdateWrapper语句,想想有什么问题
List<Long> ids = List.of(1L,2L,4L);
UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
.setSql("balance = balance - 200") //使用UpdateWrapper字符串拼接方法
.in("id",ids);
//这里不需要在实体类中写死,因此传入null
userMapper.update(null,wrapper);
这段代码的逻辑是业务逻辑,将来会在Service层中定义,如果我们这么写,相当于把SQL语句中的一部分写到业务代码中,这在绝大多数的企业开发规范中是不被允许的。
MP在写where语句时的功能很强大,但在实现where之前的语句时作用很弱,如上述例子。
如果我们不用MP,在写CRUD接口时会很麻烦;如果我们用MP,就会违背企业开发规范,因此,自定义SQL应运而生。
具体实现
- 基于Wrapper构建where条件(service层)
List<Long> ids = List.of(1L,2L,4L);
int amount = 200;
//1.构建条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().in(User::getId,ids);
//2.自定义SQL方法调用
userMapper.updateBalanceByIds(wrapper, amount);
- 在mapper方法参数中用Param注解声明wrapper变量名称,必须是ew
void updateBalanceByIds(@Param("ew") LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);
- 自定义SQL,并使用Wrapper条件
<update id="updateBalanceByIds">
UPDATE tb_user SET balance = balance - #{amount} ${ew.customSqlSegment}
</update>
总结
在where条件之外的部分,我们没有办法利用MP更方便的去实现那些SQL语句时,在不想违背企业开发规范时,使用自定义SQL拼接的方式实现数据库交互。
Service接口
MP还为我们提供了Service的接口,我们继续继承其定义好的接口,能进一步提高开发效率

如上图,用我们自定义的接口继承IService,再用我们自定义的实现类继承UserService的同时继承ServiceImpl
public interface IUserService extends IService{
...
}
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService{
...
}
比如我们需要save一个新用户,可以:
@Autowired
private IUserService userService;
User user = new User();
user.set...;
...
userService.save(user);
总结
MP的Service接口使用流程是怎样的?
-
自定义Service接口继承IService接口
-
自定义Service实现类,实现自定义接口并继承ServiceImpl类
IService开发基础业务接口
我们在前面的学习发现BaseMapper和IUserService类中有很多方法是重复的。
那么我们在实际开发中,到底应该用哪个接口提供的方法呢?
需求
基于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} | 用户id 扣减金额 | 无 |
导入依赖
<!--swagger-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>4.1.0</version>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
swagger配置信息
knife4j:
enable: true
openapi:
title: 用户管理接口文档
description: "用户管理接口文档"
email: zhanghuyi@itcast.cn
concat: 虎哥
url: https://www.itcast.cn
version: v1.0.0
group:
default:
group-name: default
api-rule: package
api-rule-resources:
- com.itheima.mp.controller
这些依赖的作用(不重要)
1.spring-boot-starter-web(Web开发的基石)
-
这是Spring Boot生态中最基础、最重要的依赖之一。它能将我们的项目变成一个Web应用程序。
-
内置功能:
-
嵌入式容器:默认内置了Tomcat,不需要额外部署服务器,直接运行main方法就能启动Web服务
-
Spring MVC:提供了处理HTTP请求的核心机制(如@RestController,@RequestMapping,@GetMapping等注解)
-
JSON转换:内置了Jackson库,能自动把Java对象(POJO)转换成JSON返回给前端
-
参数校验与异常处理:提供了数据绑定和全局异常处理的框架。
-
-
总结:没有该依赖,我们的程序就无法接收HTTP请求,也无法提供接口服务
2.knife4j-openapi2-spring-boot-starter(增强的接口文档工具)
-
自动根据我们在代码里写的注解,生成一套交互式的API文档页面
-
为什么它比Swagger强?
-
界面精美:原生的Swagger UI比较简陋,Knife4j提供了更符合国内开发者习惯的左侧树状菜单布局,非常清爽。
-
离线文档:支持导出Word、PDF、Markdown等格式
-
调试方便:直接在页面上输入参数、点击发送,就能模拟前端调用接口,连Postman都不用开
-
OpenAPI 2:这个版本是基于Swagger 2.0标准的
-
-
总结:该依赖能方便后端程序员调试自己的程序而不用依赖前端页面。
Knife4j注解
1. 模块与接口层 (Controller 层)
这些注解用于定义整个接口文件的分类和每个具体方法的含义。
@Api(tags = "xxx") : 用在 Controller 类上。
-
作用:给这一组接口起个名字(如"用户管理模块")。
-
参数:
tags = "用户管理"。
@ApiOperation : 用在 Controller 的方法上。
-
作用:描述这个接口的具体功能。
-
参数:
value = "根据ID获取用户信息"。
2. 实体与模型层 (POJO/DTO/VO 层)
当你的接口返回一个对象,或者接收一个 JSON 对象时,这些注解能让前端看清每个字段的含义和数据类型。
-
@ApiModel: 用在 实体类上。- 作用:描述这个类的用途(如"用户信息对象")。
-
@ApiModelProperty: 用在 类成员变量上。-
作用:描述字段含义、示例值、是否必填。
-
参数:
value = "用户昵称", example = "张三", required = true。
-
解决前四个需求需求
@Api(tags = "用户管理接口")
@RequestMapping("/users")
@RestController
@RequiredArgsConstructor
public class UserController{
private final IUserService userService;
//RequestBody作用是把前端传过来的JSON字符串“反序列化”成Java对象
@ApiOperation("新增用户接口")
@PostMapping
public void saveUser(@RequestBody UserFormDTO userDTO){
//1.把DTO拷贝到PO
User user = BeanUtil.copyProperties(userDTO, User.class);
//2.新增
userService.save(user);
}
//PathVariable注解告诉Spring,把路径中的id对应的值取出来赋给参数id,@ApiParam是knife4j的注解,能在API文档中展示id参数时,旁边显示“用户id”这个注释
@ApiOperation("删除用户接口")
@DeleteMapping("{id}")
public void deleteUserById(@ApiParam("用户id") @PathVariable("id") Long id){
userService.removeById(id);
}
@ApiOperation("根据id查询用户接口")
@GetMapping("{id}")
public UserVO queryUserById(@ApiParam("用户id") @PathVariable("id") Long id){
//1.查询用户PO
User user = userService.getById(id);
//2.把PO拷贝到VO
return BeanUtil.copyProperties(user, UserVO.class);
}
//当Spring发现我们的接收类型是List<Long>时,使用@RequestParam能把请求中重复出现的ids值(或者用逗号隔开的值)自动转换成一个Java集合
@ApiOperation("根据id批量查询用户接口")
@GetMapping
public List<UserVO> queryUserById(@ApiParam("用户id的集合") @RequestParam("ids") List<Long> ids){
//1.查询用户PO
List<User> users = userService.listByIds(ids);
//2.把PO拷贝到VO
return BeanUtil.copyToList(users, UserVO.class);
}
}
在大多数基本的增删改查中,使用Service提供的方法就能实现了,甚至用不到baseMapper
但是如果要实现复杂的业务需求,它可能需要有业务逻辑,就需要自定义service了。
更有甚者,一些复杂的业务,我们不仅仅要自定义service方法,还要自定义SQL语句,这个时候就要调mapper了,比如上面的第5个接口。
解决第五个需求
| 5 | 根据id扣减余额 | PUT | /users/{id}/deduction/{money} | 用户id 扣减金额 |
|---|
-
表面上看是根据拿到的用户id去扣减他的金额。但为了业务的严谨性,我们必须先把用户查出来,查出来后再看他的状态是否正常,检查正常后才能进入后续逻辑。此外,我们要想扣减用户的余额,那必须在之前先判断一下用户的余额是否充足,如果余额不足,也不能做扣减。这就是第五个接口有别于前四个接口的业务逻辑。
-
我们在做balance = balance - {money}这种SQL语句的时候,不建议在业务层去写SQL代码,所以必须通过自定义SQL语句完成更新。
Controller
@Api(tags = "用户管理接口")
@RequestMapping("/users")
@RestController
@RequiredArgsConstructor
public class UserController{
private final IUserService userService;
@ApiOperation("扣减用户余额接口")
@PutMapping("/{id}/deduction/{money}")
public void deductMoneyById(@ApiParam("用户id") @PathVariable("id") Long id
@ApiParam("扣减的金额") @PathVariable("money") Integer money){
userService.deductBalance(id, money);
}
}
Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService{
public void deductMoneyById(Long id, Integer money){
//1.查询用户
User user = getById(id);
//2.校验用户状态
if(user == null || user.getStatus() == 2){
throw new RuntimeException("用户状态异常!");
}
//3.校验余额是否充足
if(user.getBalance() < money){
throw new RuntimeException("用户余额不足!");
}
//4.扣减余额 update tb_user set balance = balance - money
baseMapper.deductBalance(id, money);
}
}
Mapper
public interface UserMapper extends BaseMapper<User>{
//这里的@Param作用是为参数起名,方便XML映射文件里的SQL语句能根据名字找到对应变量
@Update("UPDATE tb_user SET balance = balance - #{money} WHERE id = #{id}")
void deductBalance(@Param("id") Long id, @Param("money") Integer money);
}
当需求的业务逻辑相对复杂,需要自己写一些业务而MP只提供基本的CRUD,因此我们需要自定义service方法,并在里面编写业务逻辑。
什么时候要用mapper?
当我们的basemapper提供的这些方法或者被service提供的方法不足以满足增删改查需求的时候,我们需要自定义SQL语句。所以大多数情况,我们不怎么需要用mapper。
Lambda方法
lambdaQuery
需求
实现一个根据复杂条件查询用户的接口,查询条件如下:
-
name:用户名关键字
-
status:用户状态,可以为空
-
minBalance:最小余额,可以为空
-
maxBalance:最大余额,可以为空
解决
Controller
@Api(tags = "用户管理接口")
@RequestMapping("/users")
@RestController
@RequiredArgsConstructor
public class UserController{
private final IUserService userService;
@ApiOperation("根据复杂条件查询用户接口")
@GetMapping("/list")
public List<UserVO> queryUsers(UserQuery query){
//1.查询用户PO
List<User> users = userService.queryUsers(query.getName(), query.getStatus(), query,getMinBalance(), query.getMaxBalance());
//2.把PO拷贝到VO
return BeanUtil.copyToList(users, UserVO.class);
}
}
Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService{
public List<User> queryUsers(String name, Integer status, Integer minBalance, Integer maxBalance){
return lambdaQuery()
.like(name != null, User::getName, name)
.eq(status != null, User::getStatus, status)
.gt(minBalance != null, User::getBalance, minBalance)
.lt(maxBalance != null, User::getBalance, maxBalance)
.list();
}
}
lambdaUpdate
需求
改造根据id修改用户余额的接口,要求如下:
-
完成对用户状态的校验
-
完成对用户余额的校验
-
如果扣减后余额为0,则将用户status修改为冻结状态
Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService{
@Transactional
public void deductMoneyById(Long id, Integer money){
//1.查询用户
User user = getById(id);
//2.校验用户状态
if(user == null || user.getStatus() == 2){
throw new RuntimeException("用户状态异常!");
}
//3.校验余额是否充足
if(user.getBalance() < money){
throw new RuntimeException("用户余额不足!");
}
//4.扣减余额 update tb_user set balance = balance - money
int remainBalance = user.getBalance() - money;
lambdaUpdate()
.set(User::getBalance, remainBalance)
.set(remainBalance == 0, User::getStatus, 2)
.eq(User::getId, id)
.eq(User::getBalance, user.getBalance) //乐观锁
.update();
}
}