1. MyBatis-Plus动态SQL的核心价值解析
在数据库操作中,动态SQL一直是解决复杂查询条件的利器。传统MyBatis虽然提供了if/choose等标签实现动态SQL,但需要编写大量XML文件,维护成本较高。MyBatis-Plus的Wrapper体系彻底改变了这一局面,让动态SQL的编写变得前所未有的简洁高效。
我最近在电商后台系统中重构商品查询模块时,深刻体会到Wrapper的价值。原先需要3个不同XML映射文件处理的查询条件,现在只需一个Service方法配合Lambda表达式就能完成。特别是在处理前端传入的不固定查询参数时,Wrapper的表现尤为出色。
2. 环境准备与基础配置
2.1 版本匹配建议
根据当前技术生态,推荐以下版本组合:
- Spring Boot 2.7.x + MyBatis-Plus 3.5.17(生产环境稳定组合)
- Spring Boot 3.x + MyBatis-Plus 3.5.17(需要额外处理jakarta包冲突)
重要提示:避免直接使用最新版组合,特别是Spring Boot 3.x与MP的兼容性需要特别注意。我在实际项目中遇到过HikariCP连接池的兼容性问题。
2.2 基础依赖配置
Maven配置示例:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.17</version> </dependency> <!-- 分页插件必须单独声明 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-extension</artifactId> <version>3.5.17</version> </dependency>3. Wrapper体系深度解析
3.1 QueryWrapper与LambdaQueryWrapper对比
| 特性 | QueryWrapper | LambdaQueryWrapper |
|---|---|---|
| 编写方式 | 字符串字段名 | 方法引用 |
| 类型安全 | 无 | 有 |
| 重构友好度 | 低 | 高 |
| 复杂条件支持 | 强 | 强 |
| 性能差异 | 微秒级 | 纳秒级 |
实际项目中,我强烈推荐使用LambdaQueryWrapper。虽然初期学习成本略高,但在大型项目中,它能有效避免因字段名变更引发的运行时错误。
3.2 核心方法实战示例
场景:多条件商品查询
public Page<Product> searchProducts(ProductQuery query, Pageable pageable) { return lambdaQuery() .eq(query.getCategoryId() != null, Product::getCategoryId, query.getCategoryId()) .ge(query.getMinPrice() != null, Product::getPrice, query.getMinPrice()) .le(query.getMaxPrice() != null, Product::getPrice, query.getMaxPrice()) .like(StringUtils.isNotBlank(query.getKeyword()), Product::getName, query.getKeyword()) .in(query.getBrandIds() != null, Product::getBrandId, query.getBrandIds()) .orderByDesc(Product::getSales) .page(new Page<>(pageable.getPageNumber(), pageable.getPageSize())); }这个例子展示了几个关键技巧:
- 条件方法第一个参数使用布尔判断,实现真正的动态SQL
- 链式调用保持代码整洁
- 方法引用确保类型安全
4. 高级动态SQL技巧
4.1 嵌套条件处理
复杂业务场景常需要处理AND/OR嵌套逻辑:
wrapper.and(w -> w.eq("status", 1).or().eq("status", 2)) .or(w -> w.between("price", 100, 200).eq("category", "电子产品"));对应的SQL输出:
WHERE (status = 1 OR status = 2) OR (price BETWEEN 100 AND 200 AND category = '电子产品')4.2 动态SELECT字段控制
根据场景动态选择返回字段可以显著提升性能:
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); if (!needDetail) { wrapper.select(User::getId, User::getName); } else { wrapper.select(User.class, info -> !info.getColumn().equals("password")); }4.3 自定义SQL片段集成
对于特别复杂的条件,可以结合@Select注解使用:
@Select("<script>" + "SELECT * FROM product " + "<where>" + " <if test='ew != null'>${ew.customSqlSegment}</if>" + " AND status = 1" + "</where>" + "</script>") Page<Product> selectByCustomWrapper(@Param("ew") Wrapper wrapper, Page page);5. 性能优化与踩坑实录
5.1 索引命中原则
Wrapper生成的SQL要注意:
- 避免在索引列上使用函数包装:
wrapper.apply("DATE(create_time) = '2023-01-01'") - 模糊查询尽量右匹配:
wrapper.likeRight("name", "张") - IN语句元素数量控制在100以内
5.2 分页性能陷阱
MyBatis-Plus分页默认使用COUNT查询,大数据量时性能堪忧。两种优化方案:
方案1:禁用自动count
Page<Product> page = new Page<>(1, 10, false);方案2:覆盖优化count语句
@Select("SELECT COUNT(1) FROM (" + "SELECT id FROM product ${ew.customSqlSegment} LIMIT 10000" + ") temp") Long optimizeCount(@Param("ew") Wrapper wrapper);5.3 事务中的Wrapper复用
一个常见的错误是在事务中复用Wrapper实例:
// 错误示例 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getActive, true); userService.list(wrapper); // 第一次使用 wrapper.eq(User::getDeleted, false); // 修改条件 userService.list(wrapper); // 条件已污染!正确做法是每次查询创建新Wrapper实例,或使用clone()方法:
userService.list(wrapper.clone().eq(User::getDeleted, false));6. 与Spring Boot的深度集成
6.1 自动填充的最佳实践
利用MetaObjectHandler实现自动填充:
@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()); } }踩坑提醒:strictInsertFill和strictUpdateFill是3.3.0+版本新增方法,比旧版的setFieldValByName更安全。
6.2 多数据源下的Wrapper应用
在多数据源环境中,Wrapper需要特别注意:
- 不同数据库方言差异:Oracle的LIMIT需要特殊处理
- 表名前缀配置要统一
- 事务传播行为要明确
配置示例:
mybatis-plus: global-config: db-config: oracle-keywords-escape: true # Oracle关键字转义 table-prefix: t_ # 统一前缀 schema: main # 默认schema7. 复杂业务场景解决方案
7.1 动态表名处理
在SAAS系统中,动态表名是常见需求。两种实现方式:
方案1:使用ITableNameHandler(简单场景)
public class DynamicTableNameHandler implements ITableNameHandler { @Override public String dynamicTableName(String sql, String tableName) { return TenantContext.getTablePrefix() + tableName; } }方案2:SQL注入器(复杂场景)
public class TenantSqlInjector extends DefaultSqlInjector { @Override public List<AbstractMethod> getMethodList(Class<?> mapperClass) { List<AbstractMethod> methods = super.getMethodList(mapperClass); methods.add(new TenantSelectById()); return methods; } }7.2 逻辑删除的深度集成
逻辑删除配置:
mybatis-plus: global-config: db-config: logic-delete-field: deleted # 逻辑删除字段名 logic-delete-value: 1 # 删除值 logic-not-delete-value: 0 # 未删除值特殊场景处理:
// 临时忽略逻辑删除条件 wrapper.apply("1=1").eq("deleted", 1); // 查询包含已删除数据 wrapper.ignoreLogicDelete();8. 扩展与定制化开发
8.1 自定义Wrapper方法
扩展LambdaQueryWrapper:
public class MyLambdaQueryWrapper<T> extends LambdaQueryWrapper<T> { public MyLambdaQueryWrapper<T> today(String column) { super.apply("DATE({0}) = DATE(NOW())", column); return this; } public MyLambdaQueryWrapper<T> inRange(String column, Object... values) { if (values.length == 1) { return eq(column, values[0]); } return between(column, values[0], values[1]); } }8.2 SQL注入器开发
实现自定义方法:
public class BatchInsert extends AbstractMethod { @Override public MappedStatement injectMappedStatement(...) { String sql = "<script>INSERT INTO %s %s VALUES %s</script>"; // 具体实现... } }注册到配置:
@Bean public MySqlInjector mySqlInjector() { return new MySqlInjector(); }在项目实践中,我发现合理使用Wrapper可以大幅减少DAO层代码量。但也要注意避免过度封装,对于特别复杂的查询,XML映射文件仍然是更合适的选择。最新版的MyBatis-Plus 3.5.x在动态SQL方面做了很多优化,特别是对Lambda表达式的支持更加完善,值得升级体验。