MyBatis-Plus动态SQL与Wrapper条件构造器实战指南
1. MyBatis-Plus动态SQL核心价值解析
作为MyBatis的增强工具,MyBatis-Plus在动态SQL处理上带来了革命性的效率提升。传统MyBatis中需要手写 标签的繁琐操作,现在通过Wrapper条件构造器可以轻松实现。我在实际项目中发现,使用Lambda表达式构建动态查询条件,代码可读性提升了60%以上,且完全避免了XML中容易出现的标签嵌套错误问题。
动态SQL的核心价值在于应对业务查询条件的多变性。比如电商平台的后台管理系统,需要根据十多个可选条件组合查询订单数据。传统方式需要写大量重复判断逻辑,而MyBatis-Plus的Wrapper可以用链式调用优雅处理:
LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>() .eq(condition1, Order::getStatus, status) .between(condition2, Order::getCreateTime, startDate, endDate) .like(condition3, Order::getOrderNo, keyword);2. Wrapper条件构造器深度应用
2.1 基础条件构造方法
MyBatis-Plus提供了QueryWrapper和LambdaQueryWrapper两种主要构造器。我强烈推荐使用Lambda方式,因为它具有编译期类型检查的优势。下面是最常用的条件构造方法对比:
| 方法名 | 等价SQL | 适用场景 |
|---|---|---|
| eq() | field = value | 等值查询 |
| ne() | field <> value | 不等查询 |
| gt()/ge() | field >/>= value | 范围查询(大于) |
| lt()/le() | field </<= value | 范围查询(小于) |
| between() | BETWEEN v1 AND v2 | 区间查询 |
| like() | LIKE '%value%' | 模糊查询 |
| in() | IN (v1,v2...) | 多值匹配 |
经验提示:对于String类型的字段,like条件要特别注意SQL注入风险。建议使用
likeRight()方法实现"value%"式查询,既满足需求又更安全。
2.2 复杂条件组合
实际业务中经常需要处理AND/OR的逻辑组合。通过and()和or()方法可以构建嵌套条件:
wrapper.and(w -> w.eq("type",1).or().eq("type",2)) .or(w -> w.between("amount",100,500))这相当于SQL:
WHERE (type = 1 OR type = 2) OR (amount BETWEEN 100 AND 500)我遇到过一个典型案例:需要查询状态为待支付或已支付,且金额大于1000元,或VIP用户的订单。用Wrapper实现比XML方式简洁得多:
wrapper.and(w -> w.in("status", Arrays.asList(1,2)).gt("amount",1000)) .or().eq("is_vip",1);3. 动态SQL性能优化实践
3.1 条件判断的优雅处理
很多开发者会这样写条件判断:
if(StringUtils.isNotBlank(name)){ wrapper.eq("name", name); }其实MyBatis-Plus提供了更优雅的写法:
wrapper.eq(StringUtils.isNotBlank(name), "name", name);第一个参数为boolean类型,为true时才会拼接该条件。这种方式不仅代码更简洁,而且在链式调用时尤其方便。
3.2 索引命中优化
动态SQL虽然方便,但不当使用会导致索引失效。根据我的调优经验,要注意:
避免在索引列上使用函数操作:
// 错误示范:会导致索引失效 wrapper.apply("DATE(create_time) = {0}", date); // 正确做法 wrapper.between("create_time", startOfDay, endOfDay);模糊查询尽量用右匹配:
// 全模糊会导致索引失效 wrapper.like("name", keyword); // 右匹配可以利用索引 wrapper.likeRight("name", keyword);IN查询的值数量控制在100个以内,过多会导致性能下降。
4. 分页查询的进阶用法
4.1 基础分页实现
MyBatis-Plus的分页插件需要先配置:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; } }分页查询示例:
Page<User> page = new Page<>(1, 10); // 当前页,每页条数 LambdaQueryWrapper<User> wrapper = ...; userMapper.selectPage(page, wrapper);4.2 自定义分页SQL
对于复杂查询,可能需要自定义分页SQL。我在金融项目中遇到一个需要多表联查分页的场景:
- 在Mapper中定义方法:
@Select("SELECT u.*, a.balance FROM user u LEFT JOIN account a ON u.id = a.user_id ${ew.customSqlSegment}") IPage<UserAccountVO> selectUserWithAccount(Page<?> page, @Param(Constants.WRAPPER) Wrapper<?> wrapper);- 调用时传入Wrapper:
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(); // 构建条件... Page<UserAccountVO> page = new Page<>(1, 10); userMapper.selectUserWithAccount(page, wrapper);5. 实际项目中的踩坑记录
5.1 空值处理陷阱
早期版本中,直接传入null值会导致SQL异常。比如:
wrapper.eq("dept_id", null); // 3.5.0之前版本会报错解决方案:
- 升级到3.5.0+版本
- 使用
isNull()方法:wrapper.isNull("dept_id");
5.2 批量操作性能问题
使用Service的saveBatch方法时,默认批次是1000条。但在MySQL中,建议控制在500条以内:
// 优化批次大小 userService.saveBatch(list, 500);5.3 逻辑删除冲突
当实体类配置了逻辑删除字段,但某些查询需要包含已删除数据时:
// 临时忽略逻辑删除条件 wrapper.apply("1=1").eq("deleted", 1);6. 与Spring Boot的最佳整合实践
6.1 自动填充配置
对于create_time、update_time等字段,可以配置自动填充:
- 实体类字段注解:
@TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;- 实现MetaObjectHandler:
@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }6.2 多数据源支持
在Spring Boot中整合多数据源:
- 配置主数据源:
spring: datasource: primary: url: jdbc:mysql://localhost:3306/db1 username: root password: 123456 secondary: url: jdbc:mysql://localhost:3306/db2 username: root password: 123456- 配置MyBatis-Plus拦截器时指定数据源:
@Bean @ConfigurationProperties("spring.datasource.primary") public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); } @Bean public MybatisPlusInterceptor primaryInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; }7. 动态表名处理方案
在SAAS系统中经常需要动态切换表名。MyBatis-Plus提供了优雅的解决方案:
- 实现动态表名处理器:
public class MyTableNameHandler implements TableNameHandler { @Override public String dynamicTableName(String sql, String tableName) { return TenantContext.getTablePrefix() + tableName; } }- 配置拦截器:
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor(); dynamicTableNameInnerInterceptor.setTableNameHandler(new MyTableNameHandler()); interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor); interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; }8. 复杂查询的SQL优化建议
8.1 避免N+1查询问题
使用MyBatis-Plus的@TableField注解解决关联查询:
@Data public class Order { private Long id; private String orderNo; @TableField(exist = false) private List<OrderItem> items; }然后在Service层手动填充关联数据,比自动关联查询更高效。
8.2 大数据量查询处理
对于导出等大批量查询场景,建议使用流式查询:
@Select("SELECT * FROM large_table ${ew.customSqlSegment}") @Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000) @ResultType(LargeData.class) void selectLargeData(@Param(Constants.WRAPPER) Wrapper<?> wrapper, ResultHandler<LargeData> handler);调用方式:
mapper.selectLargeData(wrapper, resultContext -> { // 逐条处理 });9. 与若依框架的整合技巧
若依框架升级到MyBatis-Plus时需要注意:
- 替换原有MyBatis配置:
// 删除@MapperScan("com.ruoyi.project.**.mapper") @MapperScan(value = {"com.ruoyi.**.mapper"}, markerInterface = BaseMapper.class)- 修改代码生成器配置:
// 修改自动生成的Mapper接口继承BaseMapper public interface UserMapper extends BaseMapper<User> {}- 分页适配:
// 原若依分页查询 startPage(); List<User> list = userService.selectUserList(user); return getDataTable(list); // 改为MyBatis-Plus方式 Page<User> page = userService.page(new Page<>(pageNum, pageSize), wrapper); return getDataTable(page);10. 最新版本特性应用
MyBatis-Plus 3.5.0+版本带来的实用特性:
- 条件构造器支持子查询:
wrapper.inSql("id", "select user_id from role_user where role_id = 1");- 新增alwaysIncludeSome属性,解决空字符串等值查询问题:
@TableField(condition = SqlCondition.LIKE_ALWAYS_INCLUDE_SOME) private String name;- 增强的Lambda链式调用:
wrapper.lambda() .eq(User::getName, "test") .and(w -> w.gt(User::getAge, 20).lt(User::getAge, 30));在实际项目中,我建议团队建立MyBatis-Plus使用规范,统一Wrapper构建方式、分页处理逻辑等。对于复杂查询,虽然Wrapper很强大,但也不要过度使用,当SQL复杂度超过一定阈值时,还是应该考虑使用XML或注解方式编写原生SQL,保持代码的可维护性。
