1. MybatisPlus核心价值解析
作为Mybatis的增强工具包,MybatisPlus在持久层开发中真正做到了"开箱即用"。我在三个企业级项目中深度使用后发现,它最突出的优势在于用近乎零配置的方式解决了传统Mybatis中80%的样板代码问题。比如单表CRUD操作,原先需要手动编写接口、XML映射文件、实现类,现在只需继承BaseMapper就能自动获得18种基础方法。
重要提示:MybatisPlus并非要完全替代Mybatis,而是在其基础上进行增强。复杂SQL仍建议使用原生Mybatis方式编写。
底层原理上,MybatisPlus通过动态代理技术,在运行时自动生成Mapper接口的实现类。其核心类AbstractMethod中预置了各种数据库操作方法模板,如selectById()对应的是"SELECT * FROM table WHERE id=#{id}"的SQL模板。这种设计使得扩展非常灵活——我曾通过自定义AbstractMethod实现了逻辑删除的批量操作优化。
2. 环境搭建与基础配置
2.1 依赖引入的版本控制
SpringBoot项目中推荐使用starter方式引入。最近一个电商项目中使用的是3.5.3版本,其Gradle配置如下:
implementation 'com.baomidou:mybatis-plus-boot-starter:3.5.3' annotationProcessor 'com.baomidou:mybatis-plus-extension:3.5.3'特别注意版本兼容性:
- 3.4.x系列最后一个稳定版是3.4.3.4
- 3.5.x开始要求JDK1.8+
- 与SpringBoot版本对应关系:
SpringBoot MybatisPlus 2.0.x 3.0-3.3 2.5.x 3.4+ 3.0.x 3.5+
2.2 配置项调优实战
yml配置中这几个参数需要特别关注:
mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # SQL日志输出 default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler # 枚举处理 global-config: db-config: id-type: assign_id # 分布式ID生成 logic-delete-field: isDeleted # 逻辑删除字段 logic-not-delete-value: 0 logic-delete-value: 1在金融项目中我们遇到个坑:当字段命名包含下划线(如user_name)时,必须显式配置column-underline为true,否则自动转换会失效。
3. 核心功能深度应用
3.1 条件构造器的正确打开方式
QueryWrapper的使用看似简单,但实际项目中有几个高阶用法值得注意:
// 1. 嵌套查询条件 wrapper.nested(w -> w.eq("status",1).or().gt("create_time",localDateTime)) .apply("date_format(create_time,'%Y-%m-%d')={0}","2023-08-01"); // 2. 动态条件拼接 wrapper.eq(StringUtils.isNotBlank(name), "name", name) .between(age!=null, "age", 18, 30); // 3. 自定义SQL片段 wrapper.apply("id IN (SELECT user_id FROM user_role WHERE role_id = {0})", roleId);性能提示:避免在循环中创建Wrapper实例,应该复用同一个Wrapper对象。我们在压力测试中发现,频繁new Wrapper会导致GC压力上升。
3.2 分页查询的二次封装
官方分页虽然简单,但实际项目往往需要定制。这是我们封装的PageResult:
public PageResult<T> pageQuery(PageParam param, QueryWrapper<T> wrapper) { Page<T> mpPage = new Page<>(param.getPageNum(), param.getPageSize()); IPage<T> pageResult = mapper.selectPage(mpPage, wrapper); return new PageResult<>( pageResult.getRecords(), pageResult.getTotal(), pageResult.getCurrent(), pageResult.getSize(), pageResult.getPages() ); }特别注意分页插件必须配置:
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }4. 高级特性实战技巧
4.1 多租户方案实现
在SAAS系统中,我们通过TenantLineInnerInterceptor实现数据隔离:
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor( new TenantLineHandler() { @Override public String getTenantIdColumn() { return "tenant_id"; } @Override public Expression getTenantId() { return new StringValue(UserContext.getTenantId()); } @Override public boolean ignoreTable(String tableName) { return !"user,order".contains(tableName); } } ));4.2 乐观锁并发控制
更新库存时使用@Version注解:
@Version private Integer version;更新操作必须带上version条件:
UPDATE product SET stock=99, version=version+1 WHERE id=1 AND version=1我们在秒杀系统中实测发现,乐观锁重试次数建议控制在3次以内,否则会引发线程堆积。
5. 生产环境避坑指南
5.1 批量操作性能优化
使用SqlHelper批量执行:
List<User> userList = ...; boolean success = SqlHelper.executeBatch( User.class, log, userList, 1000, (sqlSession, entity) -> sqlSession.insert("insertUser", entity) );对比测试显示,批量插入10万条数据:
| 方式 | 耗时 |
|---|---|
| 循环单条插入 | 286s |
| MP的saveBatch | 78s |
| SqlHelper.executeBatch | 32s |
5.2 逻辑删除的索引陷阱
当使用逻辑删除时,所有查询条件都应自动带上delete=0条件。但这里有个隐藏问题——如果表上有联合索引(a,b,delete),查询条件只包含a,b时索引可能失效。解决方案是:
ALTER TABLE `user` ADD INDEX `idx_ab` (`a`, `b`); ALTER TABLE `user` ADD INDEX `idx_ab_del` (`a`, `b`, `is_deleted`);5.3 类型处理器踩坑记录
处理JSON字段时,推荐使用自定义TypeHandler:
@MappedTypes(Address.class) public class AddressTypeHandler extends BaseTypeHandler<Address> { private final ObjectMapper objectMapper = new ObjectMapper(); @Override public void setNonNullParameter(...) { ps.setString(i, objectMapper.writeValueAsString(parameter)); } // 其他方法实现... }实体类上需要注解:
@TableField(typeHandler = AddressTypeHandler.class) private Address address;6. 自定义扩展实战
6.1 自动填充策略优化
相比简单的@TableField(fill=FieldFill.INSERT),我们更推荐实现MetaObjectHandler:
@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createBy", String.class, UserContext.getUserId()); this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateBy", String.class, UserContext.getUserId()); this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }6.2 自定义SQL注入器
实现批量逻辑删除的示例:
public class LogicDeleteByIds extends AbstractMethod { @Override public MappedStatement injectMappedStatement(...) { String sql = "<script>UPDATE %s SET %s WHERE %s IN (%s)</script>"; // 具体SQL构建逻辑... return this.addDeleteMappedStatement(mapperClass, methodName, sqlSource); } } // 注册到配置 @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new MyLogicDeleteInterceptor()); return interceptor; }7. 监控与性能调优
7.1 SQL执行监控
结合p6spy实现完整SQL日志:
# application.properties spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver spring.datasource.url=jdbc:p6spy:mysql://localhost:3306/testspy.properties配置:
module.log=com.p6spy.engine.logging.P6LogFactory appender=com.p6spy.engine.spy.appender.Slf4JLogger logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat customLogMessageFormat=%(executionTime)ms | %(category) | connection %(connectionId) | %(sqlSingleLine)7.2 慢SQL预警机制
通过自定义Interceptor实现:
@Intercepts(@Signature(type= StatementHandler.class, method="query", args={Statement.class, ResultHandler.class})) public class SlowSqlInterceptor implements Interceptor { private static final long SLOW_THRESHOLD = 1000; @Override public Object intercept(Invocation invocation) throws Throwable { long start = System.currentTimeMillis(); Object result = invocation.proceed(); long cost = System.currentTimeMillis() - start; if(cost > SLOW_THRESHOLD) { StatementHandler handler = (StatementHandler) invocation.getTarget(); String sql = handler.getBoundSql().getSql(); log.warn("Slow SQL detected: {}ms - {}", cost, sql); } return result; } }8. 最佳实践总结
经过多个项目验证,这些经验值得分享:
- 简单查询用LambdaQueryWrapper,复杂SQL仍用XML方式
- 事务方法内避免多次调用Mapper方法,应尽量合并操作
- 批量操作务必控制批次大小(建议500-1000条/批)
- 多表关联查询不要强用MP,该写JOIN就写JOIN
- 生产环境必须关闭mybatis-plus.configuration.log-impl
对于新项目,我现在的标准做法是:
- 初始化时配置分页、乐观锁、性能分析插件
- 所有表包含create_time/update_time字段并配置自动填充
- 逻辑删除字段统一使用is_deleted命名
- 为常用查询条件建立复合索引