ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot集成MyBatis-Plus实战指南:从配置到分页与避坑

Spring Boot集成MyBatis-Plus实战指南:从配置到分页与避坑 简介本资源为MyBatis-Plus官方中文文档离线版面向Java后端开发者、Spring Boot项目实践者及ORM框架学习者旨在帮助用户零网络依赖、高效率掌握MP核心功能与工程化用法。文档完整覆盖自动CRUD、Lambda条件构造器、主键生成策略、分页插件、字段自动填充、乐观锁、数据权限控制等10大关键特性并提供与Spring生态集成的最佳实践说明。压缩包共140个文件以60个HTML页面含index.html、crud-interface.html、wrapper.html等核心模块和68个JS脚本支撑文档交互与搜索为主辅以少量图片PNG/JPG/GIF/SVG和样式资源CSS/ICO总大小3.77MB结构清晰、加载轻量、本地浏览流畅。目前已有3484人下载学习是快速上手、随时查阅、深入理解MyBatis-Plus设计思想与API细节的权威参考材料。1. 这不是「MyBatis说明书」而是一份能直接抄进 Spring Boot 项目的实战手册你刚接手一个老项目Mapper 层堆了 37 个 XML 文件每个都写着select * from user where id #{id}又或者你在写新模块时发现连插入一条带create_time和update_time的记录都要手动 set 时间戳、手写Insert注解——这时候打开 MyBatis-Plus 官方中文文档不是为了“学框架”而是为了一次性砍掉 80% 的样板代码。它不替代 MyBatis而是把SqlSession.selectOne()、SelectProvider、ResultMap映射配置这些重复劳动封装成一行调用userMapper.selectById(123L)。适合所有已用 MyBatis 或 Spring Boot JDBC 的团队尤其对需要快速交付 CRUD 型业务后台管理、数据中台、内部工具的开发者文档里每一页都对应着可立即落地的配置项、注解参数和条件构造器链式写法。它不教你怎么写 SQL而是告诉你当 SQL 模式固定时连 SQL 都不必写了。2. 从零启动Spring Boot MyBatis-Plus 3.5.x 的最小可行集成路径2.1 依赖声明与版本对齐策略MyBatis-Plus 的兼容性高度依赖 Spring Boot 版本。官方文档明确标注Spring Boot 2.6.x ~ 2.7.x 对应 MP 3.5.xSpring Boot 3.0 必须使用 MP 4.0JDK 17。当前主流生产环境仍以 Spring Boot 2.7.18 为主因此我们锁定mybatis-plus-boot-starter:3.5.3.12023 年 9 月发布的稳定版。Maven 依赖如下!-- pom.xml -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- 注意无需再引入 mybatis-spring-boot-starter --提示若项目已存在mybatis-spring-boot-starter必须移除否则会触发BeanDefinitionOverrideException—— MP 内部已自动注册SqlSessionFactory和SqlSessionTemplate双重注册将导致 Bean 冲突。2.2 实体类定义TableName与TableId的隐含规则MP 的自动 CRUD 基于实体类与数据库表的映射推导。默认规则是类名User→ 表名user字段userName→ 列名user_name驼峰转下划线。但实际开发中需显式控制import com.baomidou.mybatisplus.annotation.*; Table(name sys_user) // 显式指定表名避免大小写或前缀问题 public class User { TableId(type IdType.ASSIGN_ID) // 使用雪花算法生成 Long 类型主键 private Long id; TableField(fill FieldFill.INSERT) // 插入时自动填充 private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; TableLogic // 逻辑删除字段值为 0未删1已删 private Integer deleted; }注解参数说明典型场景TableId(type IdType.AUTO)数据库自增主键仅 MySQL/PostgreSQL旧系统迁移主键由 DB 控制TableId(type IdType.ASSIGN_ID)MP 默认雪花算法无序 Long分布式安全微服务集群避免 DB 单点瓶颈TableField(fill FieldFill.INSERT)仅 INSERT 时填充如create_time审计字段初始化TableLogic配合全局配置mybatis-plus.global-config.db-config.logic-delete-fielddeleted替代物理删除保障数据可追溯注意TableLogic字段必须是Integer或Boolean类型且全局配置中的logic-delete-value和logic-not-delete-value必须与数据库实际值一致如deleted0表示未删除则logic-not-delete-value0。2.3 全局配置application.yml中的 5 个关键开关MP 的行为由MybatisPlusConfig类或 YAML 配置驱动。以下配置项直接影响开发效率与 SQL 安全性# application.yml mybatis-plus: # 1. 开启 SQL 日志开发环境必开 configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 2. 全局表前缀避免每个 TableName 写 sys_ global-config: db-config: table-prefix: sys_ # 3. 逻辑删除全局开关 logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 # 4. 主键策略覆盖 TableId 的 ASSIGN_ID id-type: assign_id # 5. 自动填充处理器必须配合 MetaObjectHandler 实现 configuration: # 禁用二级缓存MP 默认关闭此处显式强调 cache-enabled: false配套的自动填充处理器需继承MetaObjectHandlerComponent public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); // 起始字段名,属性类型,值 this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }提示strictInsertFill会检查字段是否为 null 再填充避免覆盖已有值若需强制覆盖改用setFieldValByName(createTime, LocalDateTime.now(), metaObject)。3. 条件构造器实战QueryWrapper 与 LambdaQueryWrapper 的边界与选型3.1 QueryWrapper字符串字段名的安全陷阱QueryWrapper使用字符串拼接字段名编译期无法校验// ❌ 危险字段名写错不会报错运行时报 NullPointerException QueryWrapperUser wrapper new QueryWrapper(); wrapper.eq(user_name, zhangsan).gt(age, 18); // ✅ 正确但需确保字段名与数据库列名完全一致含大小写 wrapper.eq(user_name, zhangsan).orderByDesc(create_time);其适用场景是动态字段名如根据用户输入选择排序字段、多表 JOIN 后的别名字段u.user_name或与Select自定义 SQL 混用时。3.2 LambdaQueryWrapper类型安全的链式构建LambdaQueryWrapper利用 Java 8 的方法引用在编译期校验字段存在性// ✅ 编译期检查User::getUserName 必须是 User 类的有效 getter LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.eq(User::getUserName, zhangsan) .gt(User::getAge, 18) .orderByDesc(User::getCreateTime); ListUser users userMapper.selectList(wrapper);关键限制仅支持单表查询不能跨实体引用不支持SELECT u.name, r.role_name FROM user u JOIN role r ON u.role_id r.id类型的 JOIN复杂嵌套条件如(a1 AND b2) OR (c3)需用and()/or()方法包裹。3.3 复杂条件组合括号嵌套与 NULL 安全处理真实业务中常需(status 1 AND type IN (A,B)) OR (status 2 AND created_date 2023-01-01)。QueryWrapper提供and()和or()的函数式接口QueryWrapperUser wrapper new QueryWrapper(); wrapper.and(i - i.eq(status, 1).in(type, Arrays.asList(A, B))) .or(i - i.eq(status, 2).gt(created_date, 2023-01-01)); // 等价于 SQLWHERE (status 1 AND type IN (A,B)) OR (status 2 AND created_date 2023-01-01)对于可能为 null 的参数MP 提供apply()直接拼 SQL 片段String keyword request.getKeyword(); if (StringUtils.isNotBlank(keyword)) { wrapper.apply(user_name LIKE CONCAT(%, {0}, %), keyword); }注意apply()中的{0}是占位符MP 会自动进行 PreparedStatement 参数绑定防止 SQL 注入切勿用字符串拼接user_name LIKE % keyword %。4. 分页与性能优化Page 对象、分页插件配置与慢 SQL 定位4.1 Page 对象的正确用法不要在 service 层 new Page()分页必须由 MP 的IPage接口承载且Page实例需传入current页码和size每页条数// ✅ 正确controller 层接收参数并构建 Page GetMapping(/list) public ResultIPageUser list( RequestParam(defaultValue 1) long current, RequestParam(defaultValue 10) long size) { PageUser page new Page(current, size); IPageUser result userMapper.selectPage(page, new QueryWrapper()); return Result.success(result); } // ❌ 错误在 service 层 new Page() 会导致分页失效MP 无法拦截 public IPageUser listService() { PageUser page new Page(1, 10); // 这里 current1 固定无法响应前端页码 return userMapper.selectPage(page, wrapper); }4.2 分页插件配置MyBatisPlusInterceptor 的必要参数MP 4.0 统一使用MyBatisPlusInterceptor替代旧版PaginationInnerInterceptor。Spring Boot 2.7.x 下配置如下Configuration public class MybatisPlusConfig { Bean public MyBatisPlusInterceptor mybatisPlusInterceptor() { MyBatisPlusInterceptor interceptor new MyBatisPlusInterceptor(); // 添加分页插件必须 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 添加乐观锁插件可选 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } }DbType.MYSQL参数决定方言解析逻辑必须与实际数据库类型严格匹配如 PostgreSQL 用DbType.POSTGRE_SQL否则LIMIT ?,?会被错误解析为LIMIT ? OFFSET ?导致语法错误。4.3 慢 SQL 定位SQL 输出与执行耗时分析开启log-impl仅显示 SQL 文本无法判断耗时。需结合 MP 的PerformanceInterceptor已废弃或更可靠的方案启用 MyBatis 原生慢 SQL 日志# application.yml mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启性能分析仅开发环境 global-config: db-config: # 打印 SQL 执行时间单位ms sql-parser-cache: true更精准的方式是使用p6spy推荐dependency groupIdp6spy/groupId artifactIdp6spy/artifactId version3.9.1/version /dependencyspy.properties配置driverlistcom.mysql.cj.jdbc.Driver appendercom.p6spy.engine.spy.appender.FileLogger databaseDialectMySQL executionThreshold1000 # 超过 1000ms 记录为慢 SQL提示executionThreshold设为 1000 毫秒后所有执行超时的 SQL 将被写入spy.log包含完整 SQL、参数、耗时、调用栈比日志 grep 更可靠。5. 生产级避坑指南主键冲突、批量操作阈值与事务一致性5.1 雪花算法主键在高并发下的时钟回拨风险IdType.ASSIGN_ID使用 MP 内置的DefaultIdentifierGenerator基于 Snowflake其核心依赖系统时间戳。若服务器发生 NTP 校时导致时间回拨如从 10:00:05 回退到 10:00:03将触发InvalidSequenceException。解决方案生产环境禁用自动 NTP 校时改用chrony的平滑校准模式或替换为RedisIdGenerator需 Redis 支持Bean public IdentifierGenerator identifierGenerator(RedisTemplate redisTemplate) { return new RedisIdGenerator(redisTemplate, mp:ids:); }5.2insertBatch的 1000 条阈值与内存溢出userMapper.insertBatch(list)默认单次最多插入 1000 条MybatisPlusProperties.getBatchSize()超出则抛BatchUpdateException。原因在于 JDBC 的addBatch()在内存中累积 Statement大数据量易 OOM。调优步骤修改application.ymlmybatis-plus: global-config: db-config: # 批量操作最大条数建议 500~2000 batch-size: 500业务层手动分片public void batchInsert(ListUser users) { int batchSize 500; for (int i 0; i users.size(); i batchSize) { int end Math.min(i batchSize, users.size()); userMapper.insertBatch(users.subList(i, end)); } }5.3Transactional与 MP 自动填充的执行顺序冲突当 Service 方法加了Transactional且内部调用userMapper.updateById(user)时若user对象的updateTime字段已被手动 setMetaObjectHandler的updateFill将被跳过因为字段非 null。强制刷新策略在updateById前清空待更新字段user.setUpdateTime(null); // 触发 MetaObjectHandler 自动填充 userMapper.updateById(user);或改用LambdaUpdateWrapper显式设置LambdaUpdateWrapperUser wrapper new LambdaUpdateWrapper(); wrapper.set(User::getUpdateTime, LocalDateTime.now()) .eq(User::getId, userId); userMapper.update(null, wrapper);验证技巧在MetaObjectHandler.updateFill()中添加System.out.println(Filling updateTime: LocalDateTime.now())观察日志是否触发即可确认自动填充是否生效。本文还有配套的精品资源点击获取
返回列表