1. 为什么需要条件构造器在日常开发中数据库操作是绕不开的话题。记得我刚入行时每次写SQL都要手动拼接字符串不仅容易出错还经常被SQL注入漏洞困扰。后来接触到MyBatis虽然解决了安全问题但XML中写动态SQL还是不够直观。直到遇见Mybatis-Plus的条件构造器才真正体会到什么是优雅地操作数据库。QueryWrapper和UpdateWrapper这两个神器本质上是为了解决三个核心问题类型安全告别手写字段名的字符串避免拼写错误导致的运行时异常动态条件用面向对象的方式构建复杂查询条件代码更易读易维护SQL防注入自动处理参数转义从根本上杜绝SQL注入风险举个例子我们要查询年龄大于25岁且姓名包含张的用户传统方式可能要这样写String sql SELECT * FROM user WHERE age 25; if(name ! null) { sql AND name LIKE % name %; // 这里有SQL注入风险 }而使用QueryWrapper只需要QueryWrapperUser wrapper new QueryWrapper(); wrapper.gt(age, 25) .like(StringUtils.isNotBlank(name), name, name);2. QueryWrapper深度解析2.1 基础用法实战先来看个完整的示例场景电商系统中的商品搜索功能。假设我们需要实现以下查询条件价格区间筛选商品分类多选关键词模糊匹配按销量或价格排序对应的QueryWrapper实现如下public PageProduct searchProducts(ProductQuery query, PageParam page) { QueryWrapperProduct wrapper new QueryWrapper(); // 价格区间 wrapper.between(query.getMinPrice() ! null query.getMaxPrice() ! null, price, query.getMinPrice(), query.getMaxPrice()); // 分类多选 if(CollectionUtils.isNotEmpty(query.getCategoryIds())) { wrapper.in(category_id, query.getCategoryIds()); } // 关键词搜索同时匹配标题和描述 if(StringUtils.isNotBlank(query.getKeyword())) { wrapper.and(w - w.like(title, query.getKeyword()) .or() .like(description, query.getKeyword())); } // 动态排序 if(sales.equals(query.getSortBy())) { wrapper.orderByDesc(sales_count); } else { wrapper.orderByAsc(price); } return productMapper.selectPage(page, wrapper); }2.2 动态条件构建技巧实际开发中我们经常遇到参数可能为null的情况。Mybatis-Plus提供了条件方法的变体可以智能跳过空值// 传统写法需要手动判空 if(name ! null) { wrapper.eq(name, name); } // 更优雅的写法当第二个参数为null时自动跳过 wrapper.eq(StringUtils.isNotBlank(name), name, name);对于复杂的嵌套条件可以使用lambda表达式保持代码清晰wrapper.nested(qw - qw.gt(price, 100) .or() .lt(stock, 10)) .and(qw - qw.like(name, iPhone).or().like(name, iPad));生成的SQL类似于WHERE (price 100 OR stock 10) AND (name LIKE %iPhone% OR name LIKE %iPad%)2.3 多表关联查询方案虽然QueryWrapper主要针对单表操作但配合Mybatis-Plus的TableField注解也能优雅处理关联查询// 实体类中添加关联字段注解 Data public class Order { private Long id; TableField(exist false) private ListOrderItem items; } // 查询时使用apply方法拼接JOIN语句 QueryWrapperOrder wrapper new QueryWrapper(); wrapper.apply(o.user_id u.id) .inSql(u.dept_id, SELECT id FROM dept WHERE level 2);对于更复杂的多表查询建议结合XML映射文件使用保持代码可维护性。3. UpdateWrapper高级玩法3.1 条件更新实战遇到这样一个需求批量修改用户状态但只针对特定条件的用户。用UpdateWrapper可以这样实现public int batchUpdateUserStatus(ListLong ids, Integer status) { UpdateWrapperUser wrapper new UpdateWrapper(); wrapper.in(id, ids) .eq(is_deleted, 0) // 只更新未删除的用户 .set(status, status) .set(update_time, new Date()); return userMapper.update(null, wrapper); }注意这里第一个参数传null表示不更新实体对象完全由wrapper控制SET内容。生成的SQL类似于UPDATE user SET status ?, update_time ? WHERE id IN (?,?,?) AND is_deleted 03.2 字段自增/表达式更新UpdateWrapper支持更灵活的字段更新方式// 年龄1 wrapper.setSql(age age 1); // 使用函数处理字段 wrapper.setSql(name CONCAT(name, _vip)); // 条件表达式更新 wrapper.set(status ! null, status, status) .set(amount ! null, balance, balance amount);3.3 乐观锁的完美配合结合Version注解实现乐观锁时UpdateWrapper能自动处理版本号public int updateWithLock(Long id, User user) { UpdateWrapperUser wrapper new UpdateWrapper(); wrapper.eq(id, id) .eq(version, user.getVersion()) // 带上当前版本号 .set(name, user.getName()) .set(version, user.getVersion() 1); // 版本号1 int affected userMapper.update(null, wrapper); if(affected 0) { throw new OptimisticLockException(数据已被其他事务修改); } return affected; }4. 避坑指南与性能优化4.1 索引失效的常见陷阱虽然条件构造器方便但不当使用会导致索引失效// 反例对索引字段使用函数会导致索引失效 wrapper.apply(DATE(create_time) 2023-01-01); // 正解使用范围查询 wrapper.between(create_time, 2023-01-01 00:00:00, 2023-01-01 23:59:59);其他需要注意的情况避免在索引列上使用!或操作LIKE查询尽量用右模糊like prefix%组合索引要注意最左前缀原则4.2 大数据量下的分页优化当处理百万级数据时常规的LIMIT分页会越来越慢// 低效写法深度分页性能差 PageUser page new Page(10000, 10); userMapper.selectPage(page, wrapper); // 优化方案1使用last方法拼接优化提示 wrapper.last(LIMIT 10000, 10); // 优化方案2基于游标的分页记录上一页最后一条记录的ID wrapper.gt(id, lastId).orderByAsc(id).last(LIMIT 10);4.3 事务中的注意事项在Spring事务中批量操作时建议控制单次操作的数据量Transactional public void batchProcess(ListLong ids) { // 每500条执行一次避免事务过大 Lists.partition(ids, 500).forEach(batch - { UpdateWrapperUser wrapper new UpdateWrapper(); wrapper.in(id, batch) .set(processed, true); userMapper.update(null, wrapper); }); }5. 扩展应用场景5.1 动态权限过滤实现数据权限过滤的优雅方案public QueryWrapperOrder addDataPermission(QueryWrapperOrder wrapper) { User currentUser getCurrentUser(); if(!currentUser.isAdmin()) { // 普通用户只能查看自己部门的订单 wrapper.inSql(dept_id, SELECT dept_id FROM user_dept WHERE user_id currentUser.getId()); } return wrapper; }5.2 多租户SAAS应用结合Mybatis-Plus的多租户插件自动添加租户条件Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 添加多租户拦截器 interceptor.addInnerInterceptor(new TenantLineInnerInterceptor( new TenantLineHandler() { Override public String getTenantIdColumn() { return tenant_id; } Override public Expression getTenantId() { return new LongValue(1L); // 实际从上下文中获取 } } )); return interceptor; }5.3 与Lambda表达式的结合Java8的Lambda能让代码更安全// 传统方式有字段名拼写风险 wrapper.eq(createTime, date); // Lambda方式编译期检查 wrapper.lambda().eq(User::getCreateTime, date);对于复杂条件Lambda的优势更明显wrapper.lambda() .and(qw - qw.gt(User::getScore, 90).or().eq(User::getVipLevel, 3)) .ne(User::getStatus, 0);
Mybatis-Plus条件构造器实战:QueryWrapper与UpdateWrapper的进阶应用与避坑指南
1. 为什么需要条件构造器在日常开发中数据库操作是绕不开的话题。记得我刚入行时每次写SQL都要手动拼接字符串不仅容易出错还经常被SQL注入漏洞困扰。后来接触到MyBatis虽然解决了安全问题但XML中写动态SQL还是不够直观。直到遇见Mybatis-Plus的条件构造器才真正体会到什么是优雅地操作数据库。QueryWrapper和UpdateWrapper这两个神器本质上是为了解决三个核心问题类型安全告别手写字段名的字符串避免拼写错误导致的运行时异常动态条件用面向对象的方式构建复杂查询条件代码更易读易维护SQL防注入自动处理参数转义从根本上杜绝SQL注入风险举个例子我们要查询年龄大于25岁且姓名包含张的用户传统方式可能要这样写String sql SELECT * FROM user WHERE age 25; if(name ! null) { sql AND name LIKE % name %; // 这里有SQL注入风险 }而使用QueryWrapper只需要QueryWrapperUser wrapper new QueryWrapper(); wrapper.gt(age, 25) .like(StringUtils.isNotBlank(name), name, name);2. QueryWrapper深度解析2.1 基础用法实战先来看个完整的示例场景电商系统中的商品搜索功能。假设我们需要实现以下查询条件价格区间筛选商品分类多选关键词模糊匹配按销量或价格排序对应的QueryWrapper实现如下public PageProduct searchProducts(ProductQuery query, PageParam page) { QueryWrapperProduct wrapper new QueryWrapper(); // 价格区间 wrapper.between(query.getMinPrice() ! null query.getMaxPrice() ! null, price, query.getMinPrice(), query.getMaxPrice()); // 分类多选 if(CollectionUtils.isNotEmpty(query.getCategoryIds())) { wrapper.in(category_id, query.getCategoryIds()); } // 关键词搜索同时匹配标题和描述 if(StringUtils.isNotBlank(query.getKeyword())) { wrapper.and(w - w.like(title, query.getKeyword()) .or() .like(description, query.getKeyword())); } // 动态排序 if(sales.equals(query.getSortBy())) { wrapper.orderByDesc(sales_count); } else { wrapper.orderByAsc(price); } return productMapper.selectPage(page, wrapper); }2.2 动态条件构建技巧实际开发中我们经常遇到参数可能为null的情况。Mybatis-Plus提供了条件方法的变体可以智能跳过空值// 传统写法需要手动判空 if(name ! null) { wrapper.eq(name, name); } // 更优雅的写法当第二个参数为null时自动跳过 wrapper.eq(StringUtils.isNotBlank(name), name, name);对于复杂的嵌套条件可以使用lambda表达式保持代码清晰wrapper.nested(qw - qw.gt(price, 100) .or() .lt(stock, 10)) .and(qw - qw.like(name, iPhone).or().like(name, iPad));生成的SQL类似于WHERE (price 100 OR stock 10) AND (name LIKE %iPhone% OR name LIKE %iPad%)2.3 多表关联查询方案虽然QueryWrapper主要针对单表操作但配合Mybatis-Plus的TableField注解也能优雅处理关联查询// 实体类中添加关联字段注解 Data public class Order { private Long id; TableField(exist false) private ListOrderItem items; } // 查询时使用apply方法拼接JOIN语句 QueryWrapperOrder wrapper new QueryWrapper(); wrapper.apply(o.user_id u.id) .inSql(u.dept_id, SELECT id FROM dept WHERE level 2);对于更复杂的多表查询建议结合XML映射文件使用保持代码可维护性。3. UpdateWrapper高级玩法3.1 条件更新实战遇到这样一个需求批量修改用户状态但只针对特定条件的用户。用UpdateWrapper可以这样实现public int batchUpdateUserStatus(ListLong ids, Integer status) { UpdateWrapperUser wrapper new UpdateWrapper(); wrapper.in(id, ids) .eq(is_deleted, 0) // 只更新未删除的用户 .set(status, status) .set(update_time, new Date()); return userMapper.update(null, wrapper); }注意这里第一个参数传null表示不更新实体对象完全由wrapper控制SET内容。生成的SQL类似于UPDATE user SET status ?, update_time ? WHERE id IN (?,?,?) AND is_deleted 03.2 字段自增/表达式更新UpdateWrapper支持更灵活的字段更新方式// 年龄1 wrapper.setSql(age age 1); // 使用函数处理字段 wrapper.setSql(name CONCAT(name, _vip)); // 条件表达式更新 wrapper.set(status ! null, status, status) .set(amount ! null, balance, balance amount);3.3 乐观锁的完美配合结合Version注解实现乐观锁时UpdateWrapper能自动处理版本号public int updateWithLock(Long id, User user) { UpdateWrapperUser wrapper new UpdateWrapper(); wrapper.eq(id, id) .eq(version, user.getVersion()) // 带上当前版本号 .set(name, user.getName()) .set(version, user.getVersion() 1); // 版本号1 int affected userMapper.update(null, wrapper); if(affected 0) { throw new OptimisticLockException(数据已被其他事务修改); } return affected; }4. 避坑指南与性能优化4.1 索引失效的常见陷阱虽然条件构造器方便但不当使用会导致索引失效// 反例对索引字段使用函数会导致索引失效 wrapper.apply(DATE(create_time) 2023-01-01); // 正解使用范围查询 wrapper.between(create_time, 2023-01-01 00:00:00, 2023-01-01 23:59:59);其他需要注意的情况避免在索引列上使用!或操作LIKE查询尽量用右模糊like prefix%组合索引要注意最左前缀原则4.2 大数据量下的分页优化当处理百万级数据时常规的LIMIT分页会越来越慢// 低效写法深度分页性能差 PageUser page new Page(10000, 10); userMapper.selectPage(page, wrapper); // 优化方案1使用last方法拼接优化提示 wrapper.last(LIMIT 10000, 10); // 优化方案2基于游标的分页记录上一页最后一条记录的ID wrapper.gt(id, lastId).orderByAsc(id).last(LIMIT 10);4.3 事务中的注意事项在Spring事务中批量操作时建议控制单次操作的数据量Transactional public void batchProcess(ListLong ids) { // 每500条执行一次避免事务过大 Lists.partition(ids, 500).forEach(batch - { UpdateWrapperUser wrapper new UpdateWrapper(); wrapper.in(id, batch) .set(processed, true); userMapper.update(null, wrapper); }); }5. 扩展应用场景5.1 动态权限过滤实现数据权限过滤的优雅方案public QueryWrapperOrder addDataPermission(QueryWrapperOrder wrapper) { User currentUser getCurrentUser(); if(!currentUser.isAdmin()) { // 普通用户只能查看自己部门的订单 wrapper.inSql(dept_id, SELECT dept_id FROM user_dept WHERE user_id currentUser.getId()); } return wrapper; }5.2 多租户SAAS应用结合Mybatis-Plus的多租户插件自动添加租户条件Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 添加多租户拦截器 interceptor.addInnerInterceptor(new TenantLineInnerInterceptor( new TenantLineHandler() { Override public String getTenantIdColumn() { return tenant_id; } Override public Expression getTenantId() { return new LongValue(1L); // 实际从上下文中获取 } } )); return interceptor; }5.3 与Lambda表达式的结合Java8的Lambda能让代码更安全// 传统方式有字段名拼写风险 wrapper.eq(createTime, date); // Lambda方式编译期检查 wrapper.lambda().eq(User::getCreateTime, date);对于复杂条件Lambda的优势更明显wrapper.lambda() .and(qw - qw.gt(User::getScore, 90).or().eq(User::getVipLevel, 3)) .ne(User::getStatus, 0);