2026.3.11MyBatis-Plus基本使用与思考

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更快地实现了以下五个增删改查语句:

  1. 新增用户功能

  2. 根据id查询用户

  3. 根据id批量查询用户

  4. 根据id更新用户

  5. 根据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;
 }

常见配置

  1. 导入MP的类型别名的包

  2. 自定义全局配置,主要是id-type,但一般而言,注解中的配置比全局配置优先级更高

总结

MP的使用基本流程如下:

  1. 引入起步依赖

  2. 自定义Mapper基础BaseMapper

  3. 在实体类上添加注解声明 表信息

  4. 在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);
总结
  1. QueryWrapper和LambdaQueryWrapper通常用来构建select、delete、update的where条件部分

  2. UpdateWrapper和LambdaUpdateWrapper通常只有在set语句比较特殊时才使用

  3. 尽量使用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应运而生。

具体实现
  1. 基于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);
  1. 在mapper方法参数中用Param注解声明wrapper变量名称,必须是ew
复制代码
 void updateBalanceByIds(@Param("ew") LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);
  1. 自定义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 扣减金额
  1. 表面上看是根据拿到的用户id去扣减他的金额。但为了业务的严谨性,我们必须先把用户查出来,查出来后再看他的状态是否正常,检查正常后才能进入后续逻辑。此外,我们要想扣减用户的余额,那必须在之前先判断一下用户的余额是否充足,如果余额不足,也不能做扣减。这就是第五个接口有别于前四个接口的业务逻辑。

  2. 我们在做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修改用户余额的接口,要求如下:

  1. 完成对用户状态的校验

  2. 完成对用户余额的校验

  3. 如果扣减后余额为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();
     }
 }
相关推荐
RDCJM2 小时前
【MySQL】在MySQL中STR_TO_DATE()以及其他用于日期和时间的转换
android·数据库·mysql
vanvivo2 小时前
redis 使用
数据库·redis·缓存
Full Stack Developme2 小时前
Java 常用通信协议及对应的框架
java·开发语言
加成BUFF2 小时前
解决MySQL/MariaDB忘记root密码:完整重置教程(XAMPP/Windows版)
数据库·mysql·xampp
( •̀∀•́ )9202 小时前
Spring Boot 启动报错 `BindException: Permission denied`
java·spring boot·后端
杰克尼2 小时前
苍穹外卖--day10
java·数据库·spring boot·mybatis·notepad++
dreamread3 小时前
完美解决phpstudy安装后mysql无法启动
数据库·mysql
小江的记录本3 小时前
【SQL】多表关系与冷热数据(全维度知识体系)
数据库·sql·mysql·数据库开发·数据库架构
sjmaysee3 小时前
Windows操作系统部署Tomcat详细讲解
java·windows·tomcat