FEATURED · 精选文章

Spring Boot整合MyBatis-Plus:从配置到实战,提升开发效率

发布时间 / 2026/8/6 7:00:46
来源 / 创域科博编辑部
栏目 / 资讯中心
Spring Boot整合MyBatis-Plus:从配置到实战,提升开发效率 1. 项目缘起为什么是MyBatis-Plus如果你正在用Spring Boot做Java后端开发并且数据库操作还在手写SQL或者被MyBatis的XML配置和重复的CRUD代码折磨那今天这个内容就是为你准备的。我最近在重构一个老项目把原生的MyBatis换成了MyBatis-Plus整个过程下来感觉像是从手动挡换成了自动挡开发效率提升了一大截。所以我想把这次整合过程中的核心要点、踩过的坑以及一些进阶玩法系统地梳理出来。简单来说MyBatis-Plus简称MP是一个MyBatis的增强工具在MyBatis的基础上只做增强不做改变。它的核心价值在于通过极少的配置就能实现单表几乎所有的CRUD操作你甚至可以不写SQL。这对于快速开发、减少样板代码、提升团队协作效率来说意义重大。结合Spring Boot的自动配置能力两者可以说是天作之合。网上教程很多但大多只讲“怎么配”很少深入讲“为什么这么配”以及“配了之后可能会遇到什么”。这篇文章我会以一个实际项目迁移者的视角带你从零开始不仅把整合的步骤走通更要把每一步背后的逻辑和可能遇到的“暗礁”讲清楚。2. 环境搭建与依赖引入选对版本是关键整合的第一步永远是搞定依赖。这一步看似简单但版本兼容性是第一个拦路虎。根据你提供的热词很多人都在搜“mybatis-plus version3.5.17对应的springboot版本”这恰恰说明了版本匹配的重要性。2.1 依赖配置详解我以当前比较主流的Spring Boot 2.7.x版本和MyBatis-Plus 3.5.7为例3.5.17是较新版本其与Spring Boot的对应关系类似。在你的pom.xml中需要引入以下核心依赖dependencies !-- Spring Boot Web Starter (根据你的项目类型选择) -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus Spring Boot Starter -- !-- 这是核心它自动引入了MyBatis、MyBatis-Spring以及MP的核心包 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.7/version /dependency !-- 数据库驱动这里以MySQL 8为例 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- Lombok可选但强烈推荐用于简化实体类代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies为什么这么选mybatis-plus-boot-starter这个Starter是整合的关键。它基于Spring Boot的自动配置机制帮我们自动配置了SqlSessionFactory、SqlSessionTemplate、MapperScannerConfigurer等一堆Bean。如果没有它你需要手动在配置类里写一大堆Bean非常繁琐。这个Starter内部已经处理好了与Spring Boot版本的兼容性问题所以我们通常只需要关注MP本身的版本即可。版本兼容性核心原则MP的版本与MyBatis核心版本绑定。而MyBatis的版本又与Spring Boot的依赖管理中的版本有关。最稳妥的做法是去MP的官方GitHub仓库的Release页面或文档中查看其推荐的Spring Boot版本。或者直接使用Spring Boot的spring-boot-dependencies中管理的MyBatis版本然后选择与之兼容的MP版本。例如Spring Boot 2.7.18 内置的MyBatis版本可能较旧而MP 3.5.17可能需要更新版本的MyBatis。如果强行组合可能会遇到ClassNotFoundException或方法签名不匹配的错误。对于Spring Boot 3.xMP有专门的mybatis-plus-spring-boot3-starter绝对不能混用。注意如果你看到热词中提到的“dynamic-datasource 对应spring boot 4.x版本”目前Spring Boot 4.x尚未发布这很可能是个错误信息或对未来的猜测。现阶段请以官方文档为准切勿使用不存在的版本。2.2 基础配置application.yml依赖搞定后需要在application.yml中配置数据库连接和MP的一些基本行为。spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/your_database?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password mybatis-plus: configuration: # 控制台打印执行的SQL开发环境非常有用 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启下划线转驼峰映射数据库字段user_name映射到Java属性userName map-underscore-to-camel-case: true global-config: db-config: # 全局逻辑删除字段名后续会讲 logic-delete-field: deleted # 逻辑已删除值默认为1 logic-delete-value: 1 # 逻辑未删除值默认为0 logic-not-delete-value: 0 # 全局主键类型。AUTO表示数据库自增INPUT表示手动输入ASSIGN_ID表示雪花算法ASSIGN_UUID表示UUID id-type: ASSIGN_ID这里的配置项都有其作用。比如log-impl在开发阶段设置为StdOutImpl所有MP生成的SQL都会打印在控制台方便你调试和检查SQL是否正确。map-underscore-to-camel-case几乎是必开的符合Java和数据库的命名习惯。global-config下的配置是MP的全局行为比如这里预配置了逻辑删除和主键策略。3. 核心编码实体、Mapper与Service配置完成后就可以开始写代码了。MP有一套约定大于配置的编码模式理解这套模式你就掌握了MP大半的精髓。3.1 实体类Entity映射实体类是数据库表的映射。MP通过注解来识别如何与表关联。import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; Data // Lombok注解自动生成getter/setter等方法 TableName(sys_user) // 指定关联的表名如果类名是User表名也是user可省略 public class User { /** * 主键。 * TableId 注解标记主键。 * type IdType.ASSIGN_ID使用雪花算法生成Long类型ID默认策略。 * 如果数据库是自增则使用 IdType.AUTO。 */ TableId(type IdType.ASSIGN_ID) private Long id; private String username; private String password; private String email; /** * 自动填充字段。在插入或更新时由MyMetaObjectHandler自动处理。 */ TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; /** * 逻辑删除字段。并非真实删除只是用此字段标记记录是否被删除。 * TableLogic 注解声明。 * 配置了全局逻辑删除后MP的删除方法会自动变为UPDATE语句将此字段置为1。 * 查询时MP会自动加上条件 deleted 0。 */ TableLogic private Integer deleted; }关键注解解析TableName: 表名映射。在表名有前缀如t_、sys_或与类名不一致时使用。TableId:必须标注在主键字段上。type属性至关重要它决定了主键的生成策略。ASSIGN_ID雪花算法是MP的默认策略适合分布式系统。如果你的表是数据库自增主键一定要改为AUTO否则插入时会报错。TableField: 字段映射。fill属性用于配置“自动填充”这是MP一个非常实用的功能可以自动为createTime、updateTime等字段赋值无需在业务代码中手动设置。TableLogic: 逻辑删除标记。加上此注解后MP的deleteById()等方法将变为“软删除”。3.2 自动填充处理器MetaObjectHandler为了实现上面实体类中createTime和updateTime的自动填充我们需要定义一个元对象处理器。import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import java.time.LocalDateTime; Component // 声明为Spring组件 public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { // 插入操作时自动填充 // strictInsertFill方法会判断字段是否有值无值才填充避免覆盖手动设置的值 this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); // 还可以填充其他字段如创建人ID等 // this.strictInsertFill(metaObject, createBy, Long.class, UserContext.getCurrentUserId()); } Override public void updateFill(MetaObject metaObject) { // 更新操作时自动填充 this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); // this.strictUpdateFill(metaObject, updateBy, Long.class, UserContext.getCurrentUserId()); } }这个组件是MP提供的扩展点。当执行MP的insert()或update()方法时MP会回调这里的方法为带有TableField(fill ...)注解的字段自动赋值。这保证了数据创建的审计字段创建时间、更新时间的一致性是生产级应用的必备实践。3.3 Mapper接口与ServiceMP的强大之处在于你只需要一个极其简单的接口声明就能获得丰富的CRUD方法。// Mapper接口 import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; Mapper // Spring注解声明为MyBatis的Mapper会被自动扫描。也可在启动类用MapperScan批量扫描。 public interface UserMapper extends BaseMapperUser { // 无需任何方法BaseMapper已经提供了数十个通用方法。 // 如果需要自定义复杂SQL可以在这里定义方法并在对应的XML文件或使用Select注解编写SQL。 }BaseMapperUser是一个泛型接口传入你的实体类类型。继承它之后UserMapper立刻拥有了如下方法部分insert(T entity): 插入一条记录。deleteById(Serializable id): 根据主键删除逻辑删除或物理删除。updateById(T entity): 根据主键更新。selectById(Serializable id): 根据主键查询。selectList(WrapperT queryWrapper): 根据条件查询列表核心方法。selectPage(PageT page, WrapperT queryWrapper): 分页查询核心方法。Service层也可以利用MP的IService接口进行增强// Service接口 import com.baomidou.mybatisplus.extension.service.IService; public interface UserService extends IServiceUser { // 可以在此定义业务相关的特殊方法 User getUserWithDetail(Long id); } // Service实现类 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; Service public class UserServiceImpl extends ServiceImplUserMapper, User implements UserService { Override public User getUserWithDetail(Long id) { // 这里可以组合多个Mapper操作或调用baseMapper即UserMapper进行复杂查询 User user this.getById(id); // ... 其他业务逻辑 return user; } // 无需实现IService中已有的方法如save, remove, update, get, list, page等ServiceImpl已提供默认实现。 }继承ServiceImplM, T后你的Service实现类也自动获得了大量CRUD方法并且这些方法通常比Mapper层的方法更“聪明”例如save()方法会先判断主键是否存在来决定是插入还是更新。这进一步减少了样板代码。4. 实战核心QueryWrapper与分页查询这是MP日常使用频率最高的部分。QueryWrapper及其Lambda版本LambdaQueryWrapper用于构建动态查询条件而分页查询是后端开发标配。4.1 使用QueryWrapper构建动态查询假设有一个用户管理页面可以根据用户名模糊、邮箱精确、创建时间范围进行筛选。import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime; RestController public class UserController { Autowired private UserService userService; GetMapping(/users) public ListUser listUsers(RequestParam(required false) String username, RequestParam(required false) String email, RequestParam(required false) LocalDateTime createTimeStart, RequestParam(required false) LocalDateTime createTimeEnd) { // 使用LambdaQueryWrapper类型安全避免字段名拼写错误 LambdaQueryWrapperUser wrapper Wrappers.lambdaQuery(); // eq: 等于。如果email不为空则添加条件 email #{email} wrapper.eq(StringUtils.isNotBlank(email), User::getEmail, email); // like: 模糊查询。如果username不为空则添加条件 username like %#{username}% wrapper.like(StringUtils.isNotBlank(username), User::getUsername, username); // ge: 大于等于。le: 小于等于。构建时间范围查询 wrapper.ge(createTimeStart ! null, User::getCreateTime, createTimeStart); wrapper.le(createTimeEnd ! null, User::getCreateTime, createTimeEnd); // 排序按创建时间倒序 wrapper.orderByDesc(User::getCreateTime); // 执行查询。userService.list(wrapper) 内部调用的是 baseMapper.selectList(wrapper) return userService.list(wrapper); } }LambdaQueryWrapper的优势它使用了Java 8的函数式编程User::getUsername在编译时就能检查字段引用的正确性。如果改用普通的QueryWrapper你需要用字符串”username”容易拼错且重构不友好。wrapper.eq(condition, column, value)方法的设计很巧妙第一个条件为true时后面的条件才会被加入到最终的SQL中这完美契合了动态查询的需求。4.2 分页查询配置与使用分页是高频需求也是热词中大家非常关心的点mybatis-plus分页查询,springboot mybatis-plus 分页。MP的分页需要一点额外配置。第一步配置分页插件必须配置分页插件否则MP的分页方法不生效。这是一个常见的“坑”。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(); // 添加分页插件。DbType.MYSQL指定数据库类型插件会根据类型生成不同的分页SQL。 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 还可以添加其他插件如乐观锁插件 // interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } }第二步使用分页查询配置好插件后就可以在Service或Mapper中使用分页了。GetMapping(/users/page) public PageUser listUsersByPage(RequestParam(defaultValue 1) Long current, RequestParam(defaultValue 10) Long size, RequestParam(required false) String username) { // 1. 构建分页对象。current: 当前页size: 每页大小。 PageUser page new Page(current, size); // 2. 构建查询条件 LambdaQueryWrapperUser wrapper Wrappers.lambdaQuery(); wrapper.like(StringUtils.isNotBlank(username), User::getUsername, username); // 3. 执行分页查询。page对象会被MP填充总记录数、分页数据等。 PageUser resultPage userService.page(page, wrapper); // 使用Service方法 // 或者使用Mapper: userMapper.selectPage(page, wrapper); // 返回的resultPage对象包含 // resultPage.getRecords(); // 当前页的数据列表 // resultPage.getTotal(); // 总记录数 // resultPage.getCurrent(); // 当前页码 // resultPage.getSize(); // 每页大小 // resultPage.getPages(); // 总页数 return resultPage; }注意分页插件是通过拦截器机制在SQL执行前动态拼接LIMIT和COUNT语句实现的。确保你的SQL没有手动写死limit否则会干扰插件的正常工作。另外对于极其复杂的多表联查分页MP的分页可能性能不佳此时可能需要手动编写count查询或考虑其他方案。5. 进阶特性与深度避坑指南掌握了基础CRUD和分页你已经能应对80%的场景。但MP还有一些进阶特性用好了能极大提升代码质量和开发体验同时也有一些需要特别注意的“坑”。5.1 逻辑删除的“坑”与最佳实践前面我们通过TableLogic和全局配置启用了逻辑删除。这带来一个关键行为变化所有调用MP删除方法的地方实际执行的是UPDATE语句。这本身是符合预期的。容易踩的坑自定义SQL中的逻辑删除条件如果你在Mapper的XML文件中手写了SQLMP不会自动为你加上deleted 0这个条件。你必须自己在SQL的WHERE子句中加上。例如select idselectCustom resultTypeUser SELECT * FROM sys_user WHERE deleted 0 AND some_column #{value} !-- 必须手动加 -- /select联表查询在多表关联查询中逻辑删除条件也需要手动添加到每个相关表上MP无法自动处理。唯一索引冲突这是逻辑删除一个经典的业务问题。假设username字段有唯一索引你删除了用户Adeleted1后来又想新建一个同名的用户A。由于唯一索引约束的是整个表包括已逻辑删除的记录所以插入会失败。解决方案通常有移除数据库唯一索引在业务代码中保证唯一性性能有损。使用复合唯一索引包含deleted字段如UNIQUE KEY uk_username_deleted (username, deleted)但需要将deleted的未删除值设为0已删除值设为id或NULL如果允许NULL这样同名的已删除记录和未删除记录就不会冲突。这需要修改MP的全局逻辑删除值配置操作较为复杂。最佳实践建议对于简单的单表查询放心使用MP的条件构造器和通用方法。只要涉及自定义SQLXML或Select注解务必牢记逻辑删除条件并在代码审查中重点检查。在设计表结构时就提前考虑逻辑删除与唯一索引的兼容性问题。5.2 字段类型映射与自动填充的细节枚举类型处理数据库通常用tinyint或varchar存储状态码Java中我们喜欢用枚举。MP提供了EnumValue注解来优雅地映射。public enum UserStatus { DISABLED(0, 禁用), ENABLED(1, 启用); EnumValue // 标记这个字段的值存入数据库 private final Integer code; private final String desc; // ... 构造器、getter } Entity public class User { private UserStatus status; // 直接使用枚举类型 }还需要在配置中指定枚举处理器的扫描包mybatis-plus: type-enums-package: com.yourproject.enums # 你的枚举所在包这样存入数据库的是code(0/1)从数据库查询出来会自动映射为UserStatus.ENABLED或UserStatus.DISABLED对象。自动填充的“坑”自动填充处理器MetaObjectHandler中我们使用了strictInsertFill和strictUpdateFill。这两个方法是MP 3.3.0之后推荐的它们会检查目标字段是否已有值!null如果有则跳过填充。这避免了在update操作时不小心覆盖了手动设置的值。 如果你希望无论字段是否有值都强制填充可以使用fillStrategy方法但需谨慎。5.3 多数据源与事务管理结合热词思考热词中提到了“dynamic-datasource”这是一个常用的基于MP的多数据源组件。当你的项目需要连接多个数据库如主库和只读从库或不同的业务数据库时就需要用到它。整合要点引入依赖需要引入dynamic-datasource-spring-boot-starter并排除MP自带的默认数据源自动配置。配置数据源在application.yml中配置多个数据源并指定一个默认源。使用注解在Service方法或Mapper方法上使用DS(“slave”)这样的注解来切换数据源。一个核心的“坑”是事务管理Spring的Transactional注解在多数情况下与DS注解协作时事务的开启会先于数据源切换导致事务内所有操作都跑在默认数据源上造成数据错乱。解决方案经验之谈方案A推荐将需要切换数据源的操作封装在无事务或新事务的方法中。可以使用Transactional(propagation Propagation.REQUIRES_NEW)在一个新事务中执行确保数据源切换生效。方案B使用dynamic-datasource组件提供的DSTransactional注解它被设计为与DS协同工作。最根本的理解“事务边界内数据源路由键是固定的”这一原则合理设计你的Service方法粒度避免在同一个Transactional方法内交叉操作多个数据源。5.4 性能优化与SQL监控随着业务复杂SQL性能成为关键。MP虽然方便但也要警惕其可能产生的性能问题。避免N1查询这是ORM的通病。例如查询用户列表然后循环查询每个用户的部门信息。MP本身不解决此问题。你需要手动使用QueryWrapper的select()方法只查询需要的字段或者对于关联查询仍然需要编写自定义的ResultMap和SQL来实现一次查询获取所有数据。也可以考虑使用MP的“关联查询”功能需要额外依赖mybatis-plus-join但学习成本较高。监控生成的SQL开发环境开启log-impl: StdOutImpl。生产环境建议将mybatis-plus.configuration.log-impl设置为org.apache.ibatis.logging.slf4j.Slf4jImpl并结合日志框架如Logback将com.baomidou.mybatisplus包的日志级别设为DEBUG输出到日志文件便于后期排查慢SQL。警惕“全表更新/删除”使用UpdateWrapper或LambdaUpdateWrapper进行更新时如果忘记设置eq()条件MP会生成UPDATE table SET ...这样的语句导致全表更新这是灾难性的。务必在更新/删除操作前双重检查Wrapper的条件是否完备。6. 从MyBatis迁移到MyBatis-Plus的平滑过渡热词中有人提到“若依框架不分离版4.8.3版本 想将mybatis 改为 mybatis-plus”这是一个典型的迁移场景。迁移不是简单的替换jar包要有策略。迁移步骤建议依赖替换在pom.xml中将mybatis-spring-boot-starter依赖替换为mybatis-plus-boot-starter。注意版本兼容性。配置调整mybatis前缀的配置如mybatis.mapper-locations大部分可以保持不变MP兼容它。但可以新增mybatis-plus前缀的配置来启用MP特有功能。Mapper接口改造让原有的Mapper接口继承com.baomidou.mybatisplus.core.mapper.BaseMapperT。这是最关键的一步。实体类增强逐步为实体类添加TableName,TableId,TableField等注解。可以先从主键注解开始。SQL XML文件原有的XML文件完全兼容可以继续使用。你可以逐步将其中简单的CRUD SQL删除改用MP提供的方法。对于复杂SQL保留不变。渐进式重构不要试图一次性重写所有DAO层代码。可以针对新的业务功能直接使用MP风格编写对于老代码在后续维护和迭代中遇到需要修改时再将其重构为使用MP。特别注意ID生成策略检查原有表的主键生成方式自增、UUID等确保TableId的type属性配置正确。结果映射MP默认使用基于字段名的映射如果原有项目使用了大量的resultMap进行复杂的映射需要测试MP的默认行为是否能正确映射。如果不能可能需要在TableField中指定value属性或者保留原有的resultMap。整个迁移过程核心思想是“兼容并进逐步替换”。MP被设计为对MyBatis的无侵入增强这为平滑迁移提供了可能。我个人在多个项目中实践过这种迁移最大的体会是不要为了用MP而用MP。对于极其复杂的、高度优化的原生SQL如果运行良好不必强行替换。MP的价值在于消除那些简单、重复的CRUD代码让开发人员能更专注于业务逻辑。把它当成一把提高生产力的“瑞士军刀”而不是束缚手脚的“框架牢笼”这样才能用得顺手用得高效。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻