Mybatis-Plus QueryWrapper 用法指南
在 MyBatis-Plus 的日常开发中QueryWrapper作为条件构造器的核心组件彻底改变了传统的 SQL 拼接方式。它通过链式调用的 API让开发者能够以面向对象的方式动态构建 SQL 条件极大地提升了开发效率同时避免了手动拼接 SQL 带来的安全风险。一、核心概念与类体系1.1 什么是 QueryWrapperQueryWrapper是 MyBatis-Plus 提供的条件构造器专门用于构建 SQL 语句中的WHERE子句。它支持链式调用代码简洁易读动态条件自动处理空值复杂逻辑支持 AND/OR 嵌套类型安全Lambda 表达式支持1.2 Wrapper 家族体系MyBatis-Plus 提供了一系列的 Wrapper 实现针对不同的场景Wrapper抽象根类 ├── AbstractWrapper抽象实现类封装条件拼接逻辑 │ ├── QueryWrapperT // 普通查询条件构造器 │ ├── UpdateWrapperT // 更新条件构造器 │ ├── LambdaQueryWrapperT // Lambda 查询构造器推荐 │ └── LambdaUpdateWrapperT// Lambda 更新构造器 └── ...类名特点适用场景QueryWrapper使用字符串指定字段名简单查询字段固定不易出错的场景LambdaQueryWrapper使用 Lambda 方法引用指定字段复杂查询编译期检查字段名防止拼写错误UpdateWrapper支持同时构建更新 SET 和 WHERE 条件不带实体的更新操作LambdaUpdateWrapperLambda 版本的更新构造器类型安全的更新操作二、快速入门2.1 环境准备首先确保你的项目中已经引入了 MyBatis-Plus 的依赖dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency2.2 实体类定义以用户表为例定义对应的实体类Data TableName(sys_user) public class User { private Long id; private String name; private Integer age; private String email; private Integer status; private LocalDateTime createTime; private Integer deleted; // 逻辑删除字段 }2.3 Mapper 接口继承 MP 的BaseMapper即可获得通用的 CRUD 能力public interface UserMapper extends BaseMapperUser { }2.4 第一个查询示例// 1. 创建条件构造器 QueryWrapperUser wrapper new QueryWrapper(); // 2. 构建查询条件查询状态正常、年龄大于18、姓名包含张的用户 wrapper.eq(status, 1) .gt(age, 18) .like(name, 张); // 3. 执行查询 ListUser users userMapper.selectList(wrapper);上述代码会自动生成如下 SQLSELECT id,name,age,email,status,create_time,deleted FROM sys_user WHERE deleted0 AND status 1 AND age 18 AND name LIKE %张%三、基础条件方法详解3.1 比较操作这是最常用的基础条件对应 SQL 的比较运算符。方法说明SQL 等价示例eq(column, value)等于 column valueeq(name, 张三)ne(column, value)不等于 column valuene(age, 18)gt(column, value)大于 column valuegt(price, 100)ge(column, value)大于等于 column valuege(create_time, 2023-01-01)lt(column, value)小于 column valuelt(stock, 50)le(column, value)小于等于 column valuele(discount, 0.8)示例代码// 查询状态为1年龄大于等于18小于等于30的用户 wrapper.eq(status, 1) .ge(age, 18) .le(age, 30);3.2 范围查询针对集合或区间的查询条件。方法说明SQL 等价between(column, val1, val2)区间查询column BETWEEN val1 AND val2notBetween(column, val1, val2)不在区间内column NOT BETWEEN val1 AND val2in(column, values)IN 查询column IN (v1, v2, v3)notIn(column, values)NOT IN 查询column NOT IN (v1, v2, v3)inSql(column, sql)IN 子查询column IN (sql语句)示例代码// 年龄在18到30之间 wrapper.between(age, 18, 30); // 部门ID在指定列表中 wrapper.in(department_id, Arrays.asList(1, 3, 5)); // 子查询查询拥有高级角色的用户 wrapper.inSql(role_id, SELECT id FROM role WHERE level 3);3.3 模糊查询针对字符串的模糊匹配。方法说明SQL 等价like(column, value)全模糊column LIKE %value%likeLeft(column, value)左模糊column LIKE %valuelikeRight(column, value)右模糊column LIKE value%notLike(column, value)不匹配column NOT LIKE %value%示例代码// 姓名包含张 wrapper.like(name, 张); // 手机号以138开头 wrapper.likeRight(phone, 138);3.4 空值判断判断字段是否为 NULL。方法说明SQL 等价isNull(column)字段为空column IS NULLisNotNull(column)字段不为空column IS NOT NULL示例代码// 邮箱为空的用户 wrapper.isNull(email); // 更新时间不为空 wrapper.isNotNull(update_time);四、高级用法4.1 复杂逻辑组合默认情况下多个条件之间是AND关系。通过and()和or()方法可以构建复杂的嵌套逻辑。4.1.1 普通 OR 连接// 查询姓名是张三 或者 年龄是20的用户 wrapper.eq(name, 张三).or().eq(age, 20); // SQL: name 张三 OR age 204.1.2 嵌套条件括号控制优先级这是最常用的复杂场景用于控制条件的优先级。// 查询(年龄20 或者 姓名Tom) 并且 状态1 wrapper.and(w - w.eq(age, 20).or().eq(name, Tom)) .eq(status, 1);生成的 SQLWHERE (age 20 OR name Tom) AND status 14.1.3 多组嵌套// 查询(姓名包含张 且 年龄30) OR (姓名包含李 且 年龄25) wrapper.and(w - w.like(name, 张).lt(age, 30)) .or() .and(w - w.like(name, 李).lt(age, 25));4.2 LambdaQueryWrapper类型安全的选择普通的QueryWrapper使用字符串来指定字段名容易写错。LambdaQueryWrapper使用方法引用在编译期就能检查字段是否存在彻底杜绝了字段名拼写错误。// 创建 Lambda 版本的构造器 LambdaQueryWrapperUser lqw new LambdaQueryWrapper(); // 使用 User::getName 这种方法引用而不是字符串 lqw.eq(User::getStatus, 1) .ge(User::getAge, 18) .like(User::getName, 张) .orderByDesc(User::getCreateTime); ListUser users userMapper.selectList(lqw);优点编译期检查字段名写错了编译就报错不用等到运行时重构友好修改实体类字段名时IDE 会自动帮你修改这里的引用代码提示IDE 可以自动补全方法名不用手动敲字符串推荐在生产环境中优先使用LambdaQueryWrapper它能帮你避免很多低级错误。4.3 字段选择避免 SELECT *默认情况下MP 会查询所有字段。通过select方法可以只查询你需要的字段提升查询效率。// 普通方式指定字段名 wrapper.select(id, name, age); // Lambda 方式 lambdaWrapper.select(User::getId, User::getName, User::getAge);排除字段有时候字段太多排除不需要的比选择需要的更方便// 排除 password 字段查询其他所有字段 wrapper.select(User.class, info - !info.getColumn().equals(password));4.4 分组与聚合支持 GROUP BY 和 HAVING 语法。// 按年龄分组统计每个年龄的人数只保留人数大于1的分组 wrapper.groupBy(age) .having(count(*) 1) .select(age, count(*) as user_count);4.5 排序// 单字段升序 wrapper.orderByAsc(create_time); // 单字段降序 wrapper.orderByDesc(create_time); // 多字段排序先按 id 降序再按 name 升序 wrapper.orderBy(true, true, id) .orderBy(true, false, name); // 或者更简单的写法 wrapper.orderByDesc(id).orderByAsc(name);4.6 动态条件这是 QueryWrapper 最强大的功能之一所有的条件方法都支持一个condition参数当这个参数为true时才会添加这个条件。这完美解决了动态搜索的问题。public ListUser searchUsers(String name, Integer minAge, Integer maxAge, String email) { LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); // 只有当 name 不为空时才添加 like 条件 wrapper.like(StringUtils.isNotBlank(name), User::getName, name) // 只有当 minAge 不为空时才添加大于条件 .ge(minAge ! null, User::getAge, minAge) // 只有当 maxAge 不为空时才添加小于条件 .le(maxAge ! null, User::getAge, maxAge) .like(StringUtils.isNotBlank(email), User::getEmail, email); return userMapper.selectList(wrapper); }这样无论前端传了哪些参数我们都能自动构建出正确的 SQL再也不用写一堆if/else了五、实战场景示例5.1 分页查询结合 MP 的分页插件实现带条件的分页查询public IPageUser pageQuery(UserQueryDTO query) { // 1. 构建分页参数当前页每页大小 PageUser page new Page(query.getCurrent(), query.getSize()); // 2. 构建查询条件 LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(query.getName()), User::getName, query.getName()) .between(query.getMinAge() ! null query.getMaxAge() ! null, User::getAge, query.getMinAge(), query.getMaxAge()) .eq(query.getStatus() ! null, User::getStatus, query.getStatus()) .orderByDesc(User::getCreateTime); // 3. 执行分页查询 return userMapper.selectPage(page, wrapper); }5.2 批量更新使用LambdaUpdateWrapper实现批量条件更新// 将ID为1,2,3,4,5的用户状态全部改为禁用 LambdaUpdateWrapperUser wrapper Wrappers.lambdaUpdate(); wrapper.set(User::getStatus, 0) .in(User::getId, Arrays.asList(1L, 2L, 3L, 4L, 5L)); userMapper.update(null, wrapper);生成的 SQLUPDATE sys_user SET status0 WHERE id IN (1,2,3,4,5) AND deleted05.3 复杂业务查询// 需求 // 1. 年龄在18-30之间 // 2. 姓名包含张 或者 姓名包含李 // 3. 邮箱不为空 // 4. 按年龄降序排序 // 5. 只取前10条 LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.between(User::getAge, 18, 30) .and(w - w.like(User::getName, 张).or().like(User::getName, 李)) .isNotNull(User::getEmail) .orderByDesc(User::getAge) .last(limit 10); // 最后追加 limit ListUser users userMapper.selectList(wrapper);极简记忆规则以后再也不会错多个 OR 条件必须用and(() - {})包起来链式默认是 AND不用写 .and ()条件格式固定方法(条件, 字段, 值)Lambda 字段用User::getName不能乱写六、避坑指南重要6.1 SQL 注入风险危险写法直接拼接字符串到 SQL 中// 危险如果 categoryId 是用户输入的可能导致 SQL 注入 qw.apply(category_id categoryId);正确写法使用占位符{0}// 安全MP 会自动进行参数预编译 qw.apply(category_id {0}, categoryId);6.2 空值处理错误写法直接传入 null 值// 错误如果 name 是 null会生成 WHERE name null这通常不是你想要的 qw.eq(name, request.getName());正确写法使用动态条件// 正确只有 name 不为空时才添加条件 qw.eq(StringUtils.isNotBlank(name), name, name);6.3 索引失效错误写法左模糊查询会导致索引失效// 错误%mobile 这种左模糊会导致手机索引失效 qw.likeLeft(mobile, 13800138);正确写法尽量使用右模糊// 正确mobile% 这种右模糊索引可以正常生效 qw.likeRight(mobile, 13800138);6.4 嵌套条件优先级错误写法逻辑混乱OR 会把后面的条件都打乱// 错误这会变成 A1 OR B2 AND C3优先级不对 qw.eq(A,1).or().eq(B,2).eq(C,3);正确写法明确括号范围// 正确(A1 OR B2) AND C3符合预期 qw.and(wrap - wrap.eq(A,1).or().eq(B,2)) .eq(C,3);6.5 分页查询陷阱错误写法分页不排序导致分页结果混乱// 错误没有排序字段数据库返回的顺序是不确定的分页会乱 PageUser page new Page(1,10); userMapper.selectPage(page, qw);正确写法分页必须指定排序字段// 正确指定排序保证分页的稳定性 qw.orderByDesc(id); PageUser page new Page(1,10); userMapper.selectPage(page, qw);6.6 字段类型匹配错误写法字符串和数字类型不匹配// 错误数据库 status 是 int 类型你传了字符串 1 // 这会导致隐式类型转换索引失效 qw.eq(status, 1);正确写法类型严格一致// 正确使用数字类型 qw.eq(status, 1);6.7 实体属性映射错误写法字段名大小写 / 下划线搞错了// 错误实体属性是 createTime数据库字段是 create_time // 如果你没开下划线转换直接写 createTime 会找不到字段 qw.eq(createTime, value);解决方案开启全局下划线转换推荐mybatis-plus: configuration: map-underscore-to-camel-case: true直接使用 LambdaQueryWrapper彻底避免这个问题七、常用方法速查表方法说明eq等于 ne不等于 gt大于 ge大于等于 lt小于 le小于等于 like模糊匹配%value%likeLeft左模糊%valuelikeRight右模糊value%between区间查询BETWEENnotBetween不在区间NOT BETWEENinIN 查询notInNOT IN 查询isNull为空 IS NULLisNotNull不为空 IS NOT NULLgroupBy分组 GROUP BYhaving分组过滤 HAVINGorderByAsc升序排序orderByDesc降序排序select指定查询字段last追加 SQL 片段