1. 为什么SpringBoot整合MyBatis-Plus容易踩坑?
MyBatis-Plus作为MyBatis的增强工具,确实能极大简化CRUD操作,但实际整合过程中开发者常会遇到各种"坑"。根据我多年企业级项目经验,这些问题主要源于三个维度:
第一是版本兼容性问题。SpringBoot 2.x与3.x对MyBatis-Plus的依赖管理差异巨大,比如:
- SpringBoot 2.7.x默认兼容MyBatis-Plus 3.5.x
- SpringBoot 3.0+需要MyBatis-Plus 3.5.3+版本
- 而MyBatis-Plus自身的小版本迭代也会引入API变化
第二是自动配置的隐性规则。MyBatis-Plus通过MybatisPlusAutoConfiguration实现自动装配,但以下配置项经常被忽略:
yaml复制mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # SQL日志打印
global-config:
db-config:
id-type: auto # 主键策略
logic-delete-field: deleted # 逻辑删除字段
第三是分页插件的特殊要求。不同于原生MyBatis的分页实现,MyBatis-Plus的分页拦截器需要显式配置且对返回值类型敏感。我曾遇到过团队因为返回IPage还是List争论半天的情况。
关键经验:新项目启动时,务必通过start.spring.io生成标准项目结构,避免手动拼凑依赖导致的版本冲突。我习惯用以下命令验证环境:
bash复制mvn dependency:tree | grep mybatis-plus
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. CRUD操作中的高频报错与解决方案
2.1 插入数据时的典型异常
问题现象:执行userMapper.insert(user)时报java.sql.SQLException: Field 'xxx' doesn't have a default value
这是MyBatis-Plus的字段策略配置问题。默认情况下,未赋值的字段会尝试插入NULL,如果数据库字段是NOT NULL且无默认值就会报错。解决方案有三种:
- 实体类字段加
@TableField注解:
java复制@TableField(insertStrategy = FieldStrategy.IGNORED)
private String phone;
- 全局配置(application.yml):
yaml复制mybatis-plus:
global-config:
db-config:
insert-strategy: not_empty
- 手动设置字段值(最推荐):
java复制User user = new User().setName("test").setPhone(""); // 显式设置空值
2.2 更新操作的版本冲突
当使用updateById方法时,如果实体类有@Version注解字段,可能遇到乐观锁冲突。正确的处理流程应该是:
- 先查询最新数据
- 修改非版本字段
- 执行更新
java复制User current = userMapper.selectById(1L);
current.setName("newName");
int rows = userMapper.updateById(current);
if(rows == 0) {
throw new OptimisticLockException("数据已被其他事务修改");
}
2.3 批量操作的性能陷阱
很多开发者会误用saveBatch方法,实际上它的默认批量提交阈值是1000条,且不会自动回滚。生产环境建议这样优化:
java复制// 手动控制事务批量插入
@Transactional
public void batchInsert(List<User> users) {
SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH);
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
try {
users.forEach(mapper::insert);
sqlSession.commit();
} catch (Exception e) {
sqlSession.rollback();
throw e;
} finally {
sqlSession.close();
}
}
3. 分页查询的深度避坑指南
3.1 基础分页配置的正确姿势
MyBatis-Plus的分页需要两个关键配置:
- 注册分页拦截器(SpringBoot 3.x+版本):
java复制@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
- 控制层使用
IPage接收参数:
java复制@GetMapping("/users")
public IPage<User> listUsers(@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
return userService.page(new Page<>(page, size));
}
3.2 分页失效的六大原因
根据线上问题统计,分页失效主要由于:
- 未配置分页拦截器(最常见)
- 自定义SQL未使用
${ew.customSqlSegment} - 返回类型不是
IPage - 多数据源环境下拦截器未正确注入
- 使用
PageHelper和MyBatis-Plus分页混用 - 前端传参格式错误(如pageNum/pageSize命名不一致)
3.3 高性能分页优化方案
当处理百万级数据分页时,传统LIMIT方案性能极差。推荐两种优化方案:
方案一:基于主键的键集分页
sql复制SELECT * FROM user
WHERE id > #{lastId}
ORDER BY id ASC
LIMIT #{size}
方案二:使用覆盖索引
java复制// 先查ID再关联
Page<User> page = new Page<>(1, 10);
page.setSearchCount(false); // 禁用count查询
List<Long> ids = userMapper.selectIds(page);
List<User> users = userMapper.selectBatchIds(ids);
4. 条件构造器的进阶用法与坑点
4.1 Lambda表达式的最佳实践
推荐使用LambdaQueryWrapper而非普通QueryWrapper,原因有三:
- 编译时类型检查
- 代码重构友好
- 避免魔法值
java复制// 好写法
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
wrapper.eq(User::getStatus, 1)
.between(User::getCreateTime, start, end);
// 坏写法(字段名硬编码)
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("status", 1);
4.2 动态SQL的优雅实现
很多项目会写大量if-else构建条件,其实可以这样简化:
java复制public List<User> search(UserQuery query) {
return lambdaQuery()
.eq(query.getId() != null, User::getId, query.getId())
.like(StringUtils.isNotBlank(query.getName()), User::getName, query.getName())
.list();
}
4.3 连表查询的注意事项
MyBatis-Plus官方不建议用Wrapper做复杂联表,但可以通过子查询实现:
java复制// 查询部门下的用户
LambdaQueryWrapper<Department> deptWrapper = Wrappers.lambdaQuery();
deptWrapper.inSql(Department::getId,
"SELECT dept_id FROM user WHERE status = 1");
List<Department> depts = departmentMapper.selectList(deptWrapper);
5. 企业级项目中的实战经验
5.1 多租户方案的选择
根据项目规模选择租户实现方式:
| 方案 | 适用场景 | 实现复杂度 | 性能影响 |
|---|---|---|---|
| 独立数据库 | 大型SaaS | 高 | 无 |
| Schema隔离 | 中型系统 | 中 | 小 |
| 字段过滤 | 小型应用 | 低 | 中 |
动态取消租户过滤的写法:
java复制// 临时关闭租户过滤
try {
TenantHelper.disable();
// 执行跨租户查询
} finally {
TenantHelper.enable();
}
5.2 审计字段的自动化处理
通过MetaObjectHandler实现自动填充:
java复制@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "createBy", String.class, getCurrentUser());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
5.3 复杂查询的性能监控
建议在开发环境开启SQL分析拦截器:
yaml复制mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
sql-parser-cache: true
对于生产环境,可以自定义拦截器统计慢查询:
java复制@Intercepts(@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class SlowQueryInterceptor implements Interceptor {
private static final long SLOW_QUERY_THRESHOLD = 1000; // 1秒
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
Object result = invocation.proceed();
long duration = System.currentTimeMillis() - start;
if(duration > SLOW_QUERY_THRESHOLD) {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
log.warn("Slow query detected: {} took {}ms", ms.getId(), duration);
}
return result;
}
}
最后分享一个真实案例:某金融项目使用MyBatis-Plus批量插入时,由于未关闭MyBatis的一级缓存,导致内存溢出。解决方案是在批量操作时手动清空缓存:
java复制sqlSession.clearCache(); // 每1000条清空一次
这些经验都是我们在生产环境用真金白银换来的教训,希望你能少走弯路。记住,框架再强大也要理解底层原理,遇到问题时不妨DEBUG跟踪SqlSession的执行过程,往往能发现问题的本质。
