ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SpringBoot整合MyBatis-Plus:从零到生产级CRUD与高级特性实战

2026/8/15 21:50:42 拓冰建站 浏览量
SpringBoot整合MyBatis-Plus:从零到生产级CRUD与高级特性实战 1. 项目缘起为什么是MyBatis-Plus如果你正在用SpringBoot做Java后端开发并且数据库操作还在手写SQL或者用原生的MyBatis那今天这个内容可能会让你感觉“相见恨晚”。我经历过从JDBC到Hibernate再到MyBatis最后在项目里全面拥抱MyBatis-Plus后面简称MP的整个过程。每次切换都伴随着效率的显著提升和代码量的肉眼可见减少。SpringBoot的自动配置和约定大于配置的理念已经极大地简化了Java Web应用的搭建。但在数据持久层原生的MyBatis虽然灵活却免不了要写大量的XML映射文件或注解SQL以及那些重复的增删改查方法。MP的出现就像给MyBatis装上了一套“智能外骨骼”。它没有改变MyBatis的底层而是在其基础上进行了功能增强核心价值就一句话用更少的代码做更多的事同时保持MyBatis的所有灵活性。从网络热词也能看出它的流行度“mybatis-plus分页查询”、“spring boot mybatis-plus:实现数据库字段级加密”这些都是开发者们在实际项目中遇到的真实需求。MP不仅提供了开箱即用的通用CRUD方法、强大的条件构造器、分页插件还有像字段加解密、多租户、逻辑删除这些生产级功能的内置支持。它不是一个颠覆性的新框架而是一个“效率工具”让你能把精力从重复的数据库操作中解放出来更多地关注业务逻辑本身。接下来我就带你从零开始把MP丝滑地整合进你的SpringBoot项目并深入那些真正提升开发体验的细节。2. 环境搭建与项目初始化万事开头难但SpringBoot和MP的整合开头相当简单。这里我假设你使用IntelliJ IDEA作为开发工具这也是大多数Java开发者的选择。当然用VS Code或者Eclipse步骤也是大同小异。2.1 依赖配置选对版本是关键创建一个新的SpringBoot项目时在依赖选择界面你可以直接勾选“Spring Web”、“MySQL Driver”和“MyBatis Framework”。但这里有个小坑IDEA内置的“MyBatis Framework” starter通常指的是原生的MyBatis Spring Boot Starter而不是MP的。所以更稳妥的做法是先创建一个只包含Web和MySQL依赖的基础项目然后手动在pom.xml中添加MP的依赖。打开你的pom.xml添加如下依赖dependencies !-- SpringBoot Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MySQL驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- Lombok可选但强烈推荐用于简化实体类 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- MyBatis-Plus 核心依赖 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.6/version !-- 请使用当前最新稳定版 -- /dependency !-- 代码生成器按需引入用于快速生成基础代码 -- !-- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-generator/artifactId version3.5.6/version /dependency -- /dependencies版本选择心得MP的版本需要和你的SpringBoot版本大致匹配。一般来说SpringBoot 2.7.x 对应 MP 3.5.x 是没问题的。你可以去MP的官方GitHub仓库查看版本兼容性说明。直接使用最新稳定版通常是最佳实践因为它包含了最新的功能修复和安全补丁。另外我强烈推荐引入Lombok它可以通过注解自动生成Getter/Setter、构造方法等让实体类非常简洁。2.2 数据库与基础配置依赖加好后我们需要配置数据库连接。在application.yml或application.properties中配置spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/your_database?useUnicodetruecharacterEncodingutf-8useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password # MyBatis-Plus 配置 mybatis-plus: configuration: # 控制台打印完整带参数SQL开发环境开启生产环境关闭 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: # 全局逻辑删除字段名配置后删除操作变为更新此字段 logic-delete-field: is_deleted # 逻辑已删除值(默认为 1) logic-delete-value: 1 # 逻辑未删除值(默认为 0) logic-not-delete-value: 0 # 全局表前缀如果所有表都有共同前缀如 t_ # table-prefix: t_这里有几个关键点数据库时区serverTimezoneAsia/Shanghai对于国内项目非常重要可以避免因时区不一致导致的日期时间问题。SQL日志log-impl配置为StdOutImpl可以在控制台看到MP执行的实际SQL这对于调试和优化至关重要。生产环境务必关闭可以设置为org.apache.ibatis.logging.nologging.NoLoggingImpl或使用更专业的日志框架配置。逻辑删除我直接在全局配置中开启了逻辑删除。这意味着当你调用deleteById等方法时MP并不会真正执行DELETE语句而是执行UPDATE语句将is_deleted字段更新为1。这是一种非常实用的数据保护策略。如果你的表里没有这个字段或者字段名不同可以不在全局配置而在实体类字段上用TableLogic注解单独指定。2.3 实体类与Mapper的创建假设我们有一个用户表user结构如下CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, name varchar(30) DEFAULT NULL COMMENT 姓名, age int DEFAULT NULL COMMENT 年龄, email varchar(50) DEFAULT NULL COMMENT 邮箱, is_deleted tinyint DEFAULT 0 COMMENT 逻辑删除标识, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id) );对应的实体类User.javapackage com.example.demo.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; Data // Lombok注解自动生成getter, setter, toString等方法 TableName(user) // 指定对应表名如果表名和类名一致忽略大小写可省略 public class User { TableId(type IdType.AUTO) // 主键且为数据库自增 private Long id; private String name; private Integer age; private String email; TableLogic // 逻辑删除注解优先级高于全局配置 private Integer isDeleted; TableField(fill FieldFill.INSERT) // 插入时自动填充 private LocalDateTime createTime; }实体类注解详解TableId标记主键。IdType.AUTO表示数据库自增还有IdType.ASSIGN_IDMP默认使用雪花算法生成ID、IdType.INPUT手动输入等。TableLogic逻辑删除字段。如果全局配置了这里可以省略。但如果字段名不是is_deleted比如叫deleted就需要用TableLogic(value “0”, delval “1”)来指定未删除和已删除的值。TableField字段注解。fill FieldFill.INSERT表示在插入操作时自动填充这个字段的值。谁来填充需要配合一个“元对象处理器”我们稍后配置。接下来是Mapper接口UserMapper.javapackage com.example.demo.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.demo.entity.User; import org.apache.ibatis.annotations.Mapper; Mapper // 标记这是一个MyBatis的MapperSpringBoot会自动扫描 // 关键继承 BaseMapperT并传入你的实体类泛型 public interface UserMapper extends BaseMapperUser { // 到此为止基础的CRUD方法已经全部拥有了。 }这就是MP魔力的起点你不需要在这个接口里写任何方法声明。仅仅通过继承BaseMapperUser你的UserMapper就瞬间拥有了数十个通用的数据操作方法比如selectById,selectList,insert,updateById,deleteById等等。这极大地减少了样板代码。最后别忘了在SpringBoot的主应用类上添加MapperScan注解告诉MyBatis去哪里扫描Mapper接口这样就不用在每个Mapper接口上都加Mapper了。package com.example.demo; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication MapperScan(com.example.demo.mapper) // 扫描指定包下的所有Mapper接口 public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }3. 核心功能实战从CRUD到条件构造环境搭好基础代码生成后我们就可以开始体验MP的核心功能了。我会通过一个简单的Service和Controller来演示。3.1 基础CRUD与Service层封装首先创建一个Service接口和实现类这是一种更规范的做法。IUserService.java:package com.example.demo.service; import com.baomidou.mybatisplus.extension.service.IService; import com.example.demo.entity.User; public interface IUserService extends IServiceUser { // 可以在这里定义业务特有的方法 // 基础的CRUD方法同样从 IService 继承而来功能比 BaseMapper 更丰富 }UserServiceImpl.java:package com.example.demo.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.demo.entity.User; import com.example.demo.mapper.UserMapper; import com.example.demo.service.IUserService; import org.springframework.stereotype.Service; Service public class UserServiceImpl extends ServiceImplUserMapper, User implements IUserService { // 继承 ServiceImplM, T传入你的 Mapper 和 实体类泛型 // 这样ServiceImpl 已经为你实现了 IService 的所有方法 }现在在Controller里注入IUserService就可以开始测试了。UserController.java:package com.example.demo.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.demo.entity.User; import com.example.demo.service.IUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; RestController RequestMapping(/user) public class UserController { Autowired private IUserService userService; // 1. 新增 PostMapping public boolean saveUser(RequestBody User user) { // 会忽略前端传的id由数据库自增生成 return userService.save(user); } // 2. 根据ID删除逻辑删除因为配置了逻辑删除字段 DeleteMapping(/{id}) public boolean deleteUser(PathVariable Long id) { return userService.removeById(id); // 实际执行的是 UPDATE user SET is_deleted1 WHERE id? AND is_deleted0 } // 3. 根据ID更新 PutMapping public boolean updateUser(RequestBody User user) { // 更新时只会更新非null的字段 return userService.updateById(user); } // 4. 根据ID查询 GetMapping(/{id}) public User getUserById(PathVariable Long id) { return userService.getById(id); // 自动附加条件 is_deleted0 } // 5. 查询所有未逻辑删除的 GetMapping(/list) public ListUser listUsers() { return userService.list(); } }启动应用用Postman或浏览器测试一下这几个接口你会发现基础的增删改查已经全部完成而且删除是逻辑删除。这一切你几乎没有写任何SQL。3.2 条件构造器QueryWrapper与LambdaQueryWrapper简单的CRUD显然不够实际业务中充斥着各种条件查询。MP提供了强大的条件构造器让你用Java代码来构建查询条件避免手写SQL字符串的繁琐和错误。主要有两种QueryWrapper和更推荐的LambdaQueryWrapper。QueryWrapper示例基于字符串列名// 查询年龄大于18岁并且姓名包含“张”的用户 QueryWrapperUser wrapper new QueryWrapper(); wrapper.gt(age, 18) // gt: greater than .like(name, 张); ListUser userList userService.list(wrapper);这种方式的问题在于列名“age”、“name”是字符串容易写错而且IDE无法提供代码提示和重构支持。LambdaQueryWrapper示例基于Lambda表达式类型安全// 同样的查询使用Lambda方式 LambdaQueryWrapperUser lambdaWrapper new LambdaQueryWrapper(); lambdaWrapper.gt(User::getAge, 18) .like(User::getName, “张”); ListUser userList userService.list(lambdaWrapper);User::getAge是方法引用它在编译时就会检查User类是否有getAge方法完全类型安全。即使你后来把实体类的age字段改名为userAge这里会直接编译报错而字符串方式则会在运行时才出错。因此在条件不动态变化的场景下强烈推荐使用LambdaQueryWrapper。条件构造器支持非常丰富的操作eq/ne等于 / 不等于gt/ge/lt/le大于 / 大于等于 / 小于 / 小于等于between/notBetween介于之间like/notLike/likeLeft/likeRight模糊查询isNull/isNotNullin/notIngroupBy/orderByAsc/orderByDescor/and嵌套复杂条件例如一个稍复杂的查询查询年龄在20到30之间或者邮箱以“company.com”结尾并且未被删除的用户按年龄降序排列。LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.and(w - w.between(User::getAge, 20, 30) .or() .likeRight(User::getEmail, “company.com”)) .orderByDesc(User::getAge); ListUser list userService.list(wrapper);3.3 分页查询配置与使用分页是Web开发中最常见的需求之一。MP的分页插件需要显式配置才能生效。创建一个配置类MybatisPlusConfig.javapackage com.example.demo.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 添加分页插件 PaginationInnerInterceptor paginationInnerInterceptor new PaginationInnerInterceptor(); paginationInnerInterceptor.setDbType(DbType.MYSQL); // 指定数据库类型 // 设置请求的页面大于最大页后操作true调回到首页false继续请求。默认false paginationInnerInterceptor.setOverflow(false); // 设置最大单页限制数量默认500条-1不受限制 paginationInnerInterceptor.setMaxLimit(100L); interceptor.addInnerInterceptor(paginationInnerInterceptor); return interceptor; } }配置好后就可以在Service中使用了// 在Controller中 GetMapping(/page) public IPageUser getUserPage(RequestParam(defaultValue 1) Long current, RequestParam(defaultValue 10) Long size) { // 1. 创建分页对象 PageUser page new Page(current, size); // 2. 可以附加查询条件 LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.gt(User::getAge, 18); // 3. 执行分页查询 IPageUser userPage userService.page(page, wrapper); // userPage.getRecords() 获取当前页数据 // userPage.getTotal() 获取总记录数 // userPage.getPages() 获取总页数 return userPage; }分页插件会自动在执行的SQL后面加上LIMIT ? OFFSET ?。你只需要关心当前页码和每页大小MP会帮你计算好一切。4. 高级特性与生产级配置掌握了基础CRUD和条件查询你已经能应对80%的场景。但MP还有一些“高级货”能让你的代码更健壮、更优雅。4.1 自动填充与元对象处理器还记得实体类里createTime字段上的TableField(fill FieldFill.INSERT)注解吗它声明了这个字段需要在插入时自动填充比如填充当前时间。但具体怎么填充需要我们实现一个MetaObjectHandler。创建MyMetaObjectHandler.javapackage com.example.demo.handler; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import java.time.LocalDateTime; Slf4j Component // 别忘了加这个注解让Spring管理 public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { log.info(“start insert fill...”); // 判断是否有这个字段以及这个字段是否已经被手动设置了值 this.strictInsertFill(metaObject, “createTime”, LocalDateTime.class, LocalDateTime.now()); // 还可以填充其他字段比如创建人 // this.strictInsertFill(metaObject, “createBy”, String.class, “system”); } Override public void updateFill(MetaObject metaObject) { log.info(“start update fill...”); // 更新时自动填充更新时间字段 this.strictUpdateFill(metaObject, “updateTime”, LocalDateTime.class, LocalDateTime.now()); } }这样每当执行save(user)插入操作时即使user对象没有设置createTimeMP也会自动调用这个处理器为其赋上当前时间。更新操作同理。这保证了数据创建和更新时间的准确性无需业务代码关心。4.2 逻辑删除的深度解析我们在全局配置中开启了逻辑删除。它的行为是查询自动在WHERE条件后加上AND is_deleted 0。删除调用removeById等方法实际执行UPDATE table SET is_deleted 1 WHERE id ? AND is_deleted 0。更新正常更新但通常也会自动忽略已逻辑删除的数据。一个常见的坑如果你需要真正地物理删除一条数据比如清理测试数据怎么办MP提供了removeById(1L)的逻辑删除和deleteById(1L)的物理删除。但注意deleteById是BaseMapper的方法Service层默认继承的是removeById。如果你在Service中想物理删除需要先获取到底层的MapperuserService.getBaseMapper().deleteById(1L)。不过生产环境慎用物理删除。4.3 字段类型处理器与字段加解密有时Java对象中的字段类型和数据库字段类型不是简单映射。比如Java中有一个ListString的标签字段想存到数据库的一个VARCHAR字段里用逗号分隔。这时可以用TableField(typeHandler JacksonTypeHandler.class)注解配合MP对Jackson的内置支持自动序列化成JSON字符串存储读取时再反序列化回来。需要在实体类上额外加TableName(autoResultMap true)。更高级的需求是字段加解密这也是热词中提到的。比如用户的手机号、身份证号在数据库里需要加密存储。MP没有直接提供注解但可以通过实现TypeHandler接口或使用拦截器来实现。一个更简单的思路是在getter/setter方法里做加解密或者使用像Jasypt这样的库与SpringBoot整合在数据源层面进行加解密。这超出了MP本身的范围但MP的扩展性允许你通过自定义插件Interceptor在SQL执行前后对参数和结果集进行处理从而实现字段级的加解密逻辑。4.4 多租户数据隔离Tenant Line对于SaaS系统数据隔离是核心需求。MP提供了多租户插件TenantLineInnerInterceptor。它的原理是在所有涉及租户表的SQL上自动加上tenant_id ?的条件。你需要做的是在涉及多租户的表里增加一个租户ID字段如tenant_id。实现一个TenantLineHandler告诉插件当前请求的租户ID是什么通常从ThreadLocal或SecurityContext中获取。在配置类中将此插件添加到MybatisPlusInterceptor中。当配置了多租户插件后你写的所有selectList、update、delete操作MP都会自动为你拼接租户条件极大地减少了在业务代码中手动过滤的工作量和出错概率。这也是为什么“mybatis-plus 动态取消租户隔离”会成为热词因为在某些管理后台场景超级管理员需要能查询所有租户的数据这时就需要动态地、有选择地关闭租户过滤条件。5. 代码生成器效率的终极武器当你面对一个有几十张表的新项目时为每个表创建Entity、Mapper、Service、Controller会成为体力活。MP的代码生成器MyBatis-Plus Generator就是解决这个问题的利器。虽然我们在依赖中没有引入因为通常只在开发初期使用但它的使用非常简单。你可以写一个简单的Java程序来运行它import com.baomidou.mybatisplus.generator.FastAutoGenerator; import com.baomidou.mybatisplus.generator.config.OutputFile; import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; import java.util.Collections; public class CodeGenerator { public static void main(String[] args) { FastAutoGenerator.create(“jdbc:mysql://localhost:3306/your_db”, “root”, “password”) .globalConfig(builder - { builder.author(“yourName”) // 作者名 .outputDir(“D://code//mp-gen”) // 输出目录 .enableSwagger() // 开启swagger注解 .commentDate(“yyyy-MM-dd”); }) .packageConfig(builder - { builder.parent(“com.example.demo”) // 父包名 .moduleName(“system”) // 模块名 .pathInfo(Collections.singletonMap(OutputFile.xml, “D://code//mp-gen//mapper-xml”)); // XML位置 }) .strategyConfig(builder - { builder.addInclude(“user”, “role”) // 要生成的表名 .addTablePrefix(“t_”) // 过滤表前缀 .entityBuilder() .enableLombok() // 启用Lombok .enableTableFieldAnnotation() // 字段上添加注解 .controllerBuilder() .enableRestStyle(); // 生成RestController }) .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎 .execute(); } }运行这段代码它会自动连接数据库读取表结构并在指定目录下生成所有对应的Java文件和XML映射文件如果你需要。你只需要稍微调整一下生成策略就能得到一套完整的基础CRUD代码然后在此基础上添加你的业务逻辑即可。这能节省大量初期搭建时间。6. 常见问题排查与性能优化整合过程看似顺利但实际开发中总会遇到一些“坑”。这里我总结几个最常见的问题和优化点。问题一插入或更新后返回的对象ID为null现象调用save(user)后user.getId()还是null但数据库里确实有了一条新记录并且ID是自增的。原因MP默认的IdType是ASSIGN_ID雪花算法。如果你的数据库主键是自增AUTO_INCREMENT但实体类注解是TableId或默认MP可能会尝试用雪花算法生成一个ID赋值给对象但这个ID和数据库自增的ID不同所以对象里的ID没变。解决在实体类主键字段上明确指定TableId(type IdType.AUTO)。问题二更新操作时字段被意外更新为null现象你只想更新用户的姓名但执行updateById(user)后发现用户的年龄字段在数据库里变成了NULL。原因MP的更新策略是“非null更新”。即你传入的实体对象中不为null的字段才会被拼接到UPDATE语句的SET部分。如果你从前端接收了一个只有id和name的JSON对象age为null那么更新后age字段就会被设为NULL。解决推荐使用UpdateWrapper进行局部更新UpdateWrapperUser updateWrapper new UpdateWrapper(); updateWrapper.eq(“id”, 1L).set(“name”, “新名字”); userService.update(updateWrapper); // 或者Lambda方式 LambdaUpdateWrapperUser lambdaUpdate new LambdaUpdateWrapper(); lambdaUpdate.eq(User::getId, 1L).set(User::getName, “新名字”); userService.update(lambdaUpdate);在接收对象后先根据ID从数据库查出完整对象再用新值覆盖需要更新的字段最后执行updateById。问题三分页查询总数SQL执行缓慢现象当表数据量很大时SELECT COUNT(*) FROM your_table WHERE ...这个语句会非常慢。优化MP分页插件默认会先查询总数。如果业务上可以接受“不精确的总数”或者“不需要总数”可以优化。关闭分页查询总数在构造Page对象时使用PageUser page new Page(current, size, false);第三个参数为false表示不查询总数。这样就不会执行COUNT语句。自定义COUNT语句对于非常复杂的查询MP自动生成的COUNT语句可能效率低下。你可以在Mapper.xml中自己编写一个更高效的COUNT查询并通过Select注解或XML映射文件指定。MP插件会优先使用你自定义的COUNT方法。问题四复杂联表查询痛点MP的核心优势在于单表操作。对于复杂的多表关联查询特别是涉及多个条件过滤和分页使用Wrapper会变得非常笨拙甚至无法实现。解决方案退守XML/注解这是最直接的方式。在对应的Mapper接口中定义方法然后在XML文件或Select注解中编写完整的SQL。MP并不排斥这种方式它和原生MyBatis是完美共存的。使用第三方扩展社区有一些基于MP的扩展库比如mybatis-plus-join它提供了类似Wrapper的链式API来进行联表查询。但这会增加学习成本和依赖。遵循领域驱动设计在架构层面尽量避免复杂的跨聚合根查询。可以通过业务拆分、数据冗余如宽表或使用CQRS命令查询职责分离模式将复杂的查询需求转移到专门的查询模型或搜索引擎如Elasticsearch中。性能优化小贴士选择性返回字段当表字段很多但查询只需要其中几个时使用queryWrapper.select(“id”, “name”, “age”)来指定返回的字段避免SELECT *带来的网络和内存开销。批量操作MP Service提供了saveBatch、updateBatchById等方法。在需要插入或更新大量数据时务必使用批量操作并合理设置批处理大小默认是1000这比在循环中单条操作性能高出几个数量级。监控SQL始终开启开发环境的SQL日志观察MP生成的SQL是否符合你的预期。警惕N1查询问题在循环中查询数据库这类问题通常需要通过优化业务逻辑或使用queryWrapper.in()一次性查询来解决。整合MyBatis-Plus到SpringBoot本质上是一个“提效”和“规范”的过程。它用一套约定和自动化工具把开发者从重复劳动中解放出来。但切记它不是一个银弹理解其原理和边界知道何时该用它何时该回归原生的MyBatis甚至更底层的JDBC才是一个成熟开发者的标志。从简单的CRUD到复杂条件查询再到多租户、字段填充等生产特性MP提供了一套渐进式的解决方案。我的建议是在新项目中可以大胆引入对于老项目可以从非核心模块开始尝试逐步体会它带来的便利。