MyBatis-Plus QueryWrapper 动态查询条件构造器详解与实战

MyBatis-Plus QueryWrapper 动态查询条件构造器详解与实战 1. 项目概述为什么我们需要 QueryWrapper如果你用过 MyBatis肯定对写 XML 映射文件里那些复杂的动态 SQL 标签if,choose,where又爱又恨。爱的是它功能强大恨的是维护起来太麻烦尤其是当查询条件稍微复杂一点那个 XML 文件就变得又长又难以阅读更别提调试了。MyBatis-Plus简称 MP的出现很大程度上就是为了解决这个痛点而QueryWrapper就是它送给我们的一把“瑞士军刀”。简单来说QueryWrapper是 MyBatis-Plus 提供的一个核心查询条件构造器。它的核心价值在于让我们能够用纯 Java 代码、以链式调用的方式动态地、类型安全地构建 SQL 的 WHERE 条件部分。你再也不用在 XML 里拼接那些让人眼花缭乱的字符串了。想象一下你要根据前端传过来的几个可选参数比如用户名、状态、创建时间范围来查询用户列表。用传统方式你得在 XML 里写一堆if test”username ! null” and username #{username} /if。而用QueryWrapper你只需要在 Service 层写几行清晰明了的 Java 代码逻辑一目了然。这不仅仅是代码风格的变化它带来了几个实实在在的好处第一逻辑更集中查询条件构建和业务逻辑写在同一个地方避免了在 Java 代码和 XML 文件之间反复横跳第二类型安全编译器能在编码阶段就帮你发现一些低级错误比如字段名拼写错误配合 Lambda 表达式更佳第三防止 SQL 注入QueryWrapper底层使用的是预编译语句参数全部通过占位符?传递安全性有保障第四极高的灵活性可以轻松应对各种复杂、多变的查询场景。所以无论你是刚接触 MyBatis-Plus 的新手还是已经用过但想更深入掌握的老手彻底搞懂QueryWrapper都是提升开发效率和代码质量的关键一步。接下来我们就把它拆开揉碎了从基础用法到高阶技巧再到实战避坑完整地过一遍。2. QueryWrapper 核心设计与思路拆解2.1 设计哲学告别 XML 动态 SQLMyBatis-Plus 的设计者深刻理解到虽然 XML 的动态 SQL 功能强大但它破坏了代码的连贯性和可读性。QueryWrapper的设计哲学就是将 SQL 查询条件的构建过程重新拉回到 Java 的面向对象世界里。它本质上是一个条件包装器内部维护了一系列的条件表达式比如eq、like、between最终在调用 MP 的selectList、update等方法时由 MP 的 SQL 注入器将这些条件翻译成标准的 SQL WHERE 子句并安全地填充参数。这种设计带来了声明式编程的体验。你不需要关心 SQL 字符串如何拼接只需要声明你想要什么条件。例如queryWrapper.eq(“status”, 1).like(“name”, “张”)这行代码清晰地表达了“查询状态为1且名字包含‘张’的记录”的意图。代码即文档阅读起来非常顺畅。2.2 核心架构Wrapper 体系与 Lambda 加持QueryWrapper并非孤立的它属于 MP 强大的Wrapper体系。这个体系主要包含QueryWrapperT: 用于构建查询条件是本文的重点。UpdateWrapperT: 用于构建更新条件可以指定更新哪些字段以及更新的值同样支持条件构造。LambdaQueryWrapperT和LambdaUpdateWrapperT: 这是QueryWrapper和UpdateWrapper的“语法糖”升级版。它们通过 Lambda 表达式引用实体类的属性如User::getName彻底解决了字段名硬编码的字符串问题。编译器会检查属性是否存在重构重命名字段时也能自动更新是生产环境的首选。为什么推荐 Lambda 方式假设你的用户表字段从user_name改成了username。如果你用的是字符串“user_name”你需要全局搜索替换很容易漏改。但如果你用的是User::getUserNameIDE 的重构功能可以一键安全地完成修改。这是从“运行时错误”到“编译时错误”的质变能极大提升代码的健壮性。2.3 与 MyBatis 原生方式的对比思考很多从纯 MyBatis 转过来的开发者会问用了QueryWrapper那 XML 里的动态 SQL 标签是不是就没用了并非如此它们是互补关系适用于不同场景。QueryWrapper的战场在Service 层或 Mapper 层处理动态的、业务逻辑相关的查询条件。这些条件往往根据前端请求参数、业务流程状态实时变化。例如管理后台的复合筛选查询。XML 动态 SQL 的战场处理极其复杂的查询比如涉及多重嵌套子查询、复杂的联表逻辑尽管 MP 也有连表能力但复杂场景下 XML 更直观、或者需要直接编写特定数据库方言的优化 SQL 时。另外对于固定的、复杂的查询语句放在 XML 中管理也是一种选择。我的经验是80% 的日常 CRUD 和动态查询都可以用QueryWrapper优雅地解决。剩下的 20% 复杂场景再交给 XML 或自定义 SQL。这样既能享受QueryWrapper的便捷与安全又不失灵活性。3. 核心细节解析与实操要点3.1 基础条件构造方法全解QueryWrapper提供了一整套语义化的方法来构建条件这些方法名基本与 SQL 关键字对应学习成本很低。1. 等值与非等值查询eq(column, value): 等于。WHERE column valuene(column, value): 不等于。WHERE column valuegt(column, value): 大于。ge(column, value): 大于等于。lt(column, value): 小于。le(column, value): 小于等于。// 查询年龄等于18的用户 queryWrapper.eq(“age”, 18); // 查询状态不等于0已删除的用户 queryWrapper.ne(“status”, 0);2. 范围查询between(column, value1, value2): 介于两者之间包含边界。notBetween(column, value1, value2): 不介于两者之间。in(column, Collection? valueCollection): 在集合中。这是高频且易错点。notIn(column, Collection? valueCollection): 不在集合中。// 查询年龄在20到30岁之间的用户 queryWrapper.between(“age”, 20, 30); // 查询状态为1或2的用户 ListInteger statusList Arrays.asList(1, 2); queryWrapper.in(“status”, statusList); // 注意参数是Collection注意in方法传入空集合Collection时MP 的行为需要特别注意。默认情况下MP 可能会生成WHERE column IN ()这样的非法 SQL导致数据库报错。安全的做法是在业务代码中判断集合是否为空如果为空则不应该添加in条件或者使用queryWrapper.in(CollectionUtils.isNotEmpty(list), column, list)这种条件方法后面会讲。3. 模糊查询like(column, value):WHERE column LIKE ‘%value%’。notLike(column, value): 不包含。likeLeft(column, value):WHERE column LIKE ‘%value’左模糊常用于后缀匹配。likeRight(column, value):WHERE column LIKE ‘value%’右模糊常用于前缀匹配如搜索“张”姓用户。// 查询名字中包含“技术”的用户 queryWrapper.like(“name”, “技术”); // 查询以“139”开头的手机号 queryWrapper.likeRight(“phone”, “139”);4. 空值判断isNull(column): 字段为 NULL。isNotNull(column): 字段不为 NULL。5. 分组与排序groupBy(columns…): 分组。可以传入多个字段。orderByAsc(columns…): 按字段升序排序。orderByDesc(columns…): 按字段降序排序。orderBy(boolean condition, boolean isAsc, columns…): 更灵活的条件排序。// 按部门分组并按创建时间降序排序 queryWrapper.groupBy(“dept_id”).orderByDesc(“create_time”);3.2 高级条件构造AND、OR 与嵌套复杂的业务查询很少是简单的 AND 连接经常需要 OR 逻辑和条件分组。1. AND 连接链式调用默认就是 AND 连接。queryWrapper.eq(“dept_id”, 1).gt(“salary”, 5000); // WHERE dept_id 1 AND salary 50002. OR 连接使用or()方法。or()有两种用法主动or(): 连接前后两个条件or()后面的条件与前面的条件用 OR 连接。queryWrapper.eq(“role”, “admin”).or().eq(“role”, “super_admin”); // WHERE role ‘admin’ OR role ‘super_admin’or(ConsumerParam consumer): 这是一个高阶用法用于构建一个 OR 嵌套条件组。它接受一个函数式接口在函数内部构建的条件会被括号()包裹并与外部条件用 OR 连接。queryWrapper.eq(“status”, 1) .or(i - i.eq(“name”, “张三”).ne(“age”, 10)); // WHERE status 1 OR (name ‘张三’ AND age 10) // 注意i.eq().ne() 之间是 AND 关系因为它们在同一lambda表达式内。3. 嵌套条件括号使用nested(ConsumerParam consumer)可以显式地添加括号用于控制优先级。queryWrapper.eq(“type”, “A”) .and(i - i.gt(“score”, 90).or().lt(“score”, 60)); // WHERE type ‘A’ AND (score 90 OR score 60) // 这里用 and 嵌套效果和 nested 类似清晰表达了“类型为A且分数大于90或小于60”的逻辑。实操心得当查询条件非常复杂时我建议先在纸上或注释里把想要的 SQL WHERE 子句写出来理清 AND、OR 的优先级和分组关系然后再用QueryWrapper去实现。这样可以避免构造出逻辑错误的查询条件。特别是混合使用and和or时善用nested或or(Consumer)来加括号能确保逻辑和你的预期一致。3.3 条件判断与动态 SQL 构建这是QueryWrapper最精髓的部分之一如何优雅地处理前端传来的、可能为空的查询参数MP 提供了带boolean condition参数的重载方法。// 传统做法在代码里判断 if (StringUtils.isNotBlank(username)) { queryWrapper.eq(“username”, username); } if (startTime ! null) { queryWrapper.ge(“create_time”, startTime); } // MP 优雅做法使用条件参数 queryWrapper.eq(StringUtils.isNotBlank(username), “username”, username) .ge(startTime ! null, “create_time”, startTime);eq(boolean condition, String column, Object val)这个方法当condition为true时条件生效为false时该条件被忽略。这样写代码非常简洁一行对应一个条件逻辑清晰。所有支持条件构造的方法都有这个重载版本。更进一步复用条件判断逻辑如果你的多个查询方法有类似的条件判断比如都根据时间范围查询可以将其封装成一个工具方法返回一个ConsumerQueryWrapperT然后在各个方法中通过wrapper.apply(consumer)来复用。// 定义条件片段 public static T ConsumerQueryWrapperT createTimeRangeCondition(Date start, Date end) { return w - w.ge(start ! null, “create_time”, start) .le(end ! null, “create_time”, end); } // 在业务方法中使用 public ListUser queryUsers(String name, Date start, Date end) { QueryWrapperUser qw new QueryWrapper(); qw.like(StringUtils.isNotBlank(name), “name”, name) .apply(createTimeRangeCondition(start, end)); // 应用公共条件 return userMapper.selectList(qw); }4. 实操过程与核心环节实现4.1 环境准备与基础配置假设我们有一个 Spring Boot 项目已经引入了 MyBatis-Plus 依赖。我们以一个User用户实体和对应的UserMapper为例。1. 实体类 (User.java):Data // Lombok 注解 TableName(“sys_user”) // 指定表名如果表名和类名符合MP规则下划线转驼峰可省略 public class User { TableId(type IdType.AUTO) // 主键自增 private Long id; private String username; private String email; private Integer age; private Integer status; // 状态0-禁用1-正常 private Long deptId; TableField(fill FieldFill.INSERT) // 自动填充 private LocalDateTime createTime; }2. Mapper 接口 (UserMapper.java):Repository // 可加可不加Spring Boot 会自动扫描 public interface UserMapper extends BaseMapperUser { // 继承了 BaseMapper就已经拥有了 CRUD 方法 // 可以在此定义自定义的查询方法配合 Select 注解或 XML }3. Service 层查询示例我们实现一个综合查询场景根据用户名模糊、状态、年龄范围、部门ID列表以及创建时间范围来分页查询用户并按创建时间倒序排列。Service RequiredArgsConstructor // Lombok 构造器注入 public class UserServiceImpl implements UserService { private final UserMapper userMapper; public PageUser queryUserPage(UserQueryDTO queryDTO) { // 1. 构建分页对象 (页码 页大小) PageUser page new Page(queryDTO.getPageNum(), queryDTO.getPageSize()); // 2. 构建 QueryWrapper (推荐使用 LambdaQueryWrapper) LambdaQueryWrapperUser lqw new LambdaQueryWrapper(); // 3. 动态构建查询条件 // 用户名模糊查询 lqw.like(StringUtils.isNotBlank(queryDTO.getUsername()), User::getUsername, queryDTO.getUsername()); // 状态精确查询 lqw.eq(queryDTO.getStatus() ! null, User::getStatus, queryDTO.getStatus()); // 年龄范围查询 lqw.ge(queryDTO.getMinAge() ! null, User::getAge, queryDTO.getMinAge()); lqw.le(queryDTO.getMaxAge() ! null, User::getAge, queryDTO.getMaxAge()); // 部门ID列表查询 (IN) if (CollectionUtils.isNotEmpty(queryDTO.getDeptIds())) { lqw.in(User::getDeptId, queryDTO.getDeptIds()); } // 创建时间范围查询 (BETWEEN) lqw.ge(queryDTO.getStartTime() ! null, User::getCreateTime, queryDTO.getStartTime()); lqw.le(queryDTO.getEndTime() ! null, User::getCreateTime, queryDTO.getEndTime()); // 4. 排序 lqw.orderByDesc(User::getCreateTime); // 5. 执行分页查询 return userMapper.selectPage(page, lqw); } } // 查询参数封装对象 Data public class UserQueryDTO { private String username; private Integer status; private Integer minAge; private Integer maxAge; private ListLong deptIds; private LocalDateTime startTime; private LocalDateTime endTime; private Long pageNum 1L; private Long pageSize 10L; }这段代码展示了LambdaQueryWrapper在生产环境中的典型用法。每个条件都带有前置判断避免了参数为空时生成无意义的条件如WHERE username LIKE ‘%%’。代码清晰、类型安全、易于维护。4.2 复杂查询场景实现子查询与自定义 SQL 片段QueryWrapper也支持子查询主要通过inSql、exists、notExists等方法实现。场景查询属于“研发部”这个部门的所有用户。 假设我们有部门表sys_dept其中dept_name为“研发部”的部门 ID 是 100。// 使用 inSql子查询返回单个字段id列表 LambdaQueryWrapperUser lqw new LambdaQueryWrapper(); lqw.inSql(User::getDeptId, “SELECT id FROM sys_dept WHERE dept_name ‘研发部’”); ListUser users userMapper.selectList(lqw);inSql方法直接传入一个子查询 SQL 字符串。注意这里要确保子查询 SQL 的语法正确且字段匹配。这种方式简单但 SQL 字符串硬编码可读性和维护性稍差。更优雅的方式使用QueryWrapper本身进行子查询MP 3.x 以上版本MP 支持在apply方法或条件方法中嵌套另一个Wrapper来构建子查询但这通常需要结合select方法明确指定子查询的字段。// 示例查询年龄大于平均年龄的用户 LambdaQueryWrapperUser subQuery new LambdaQueryWrapper(); subQuery.selectFunc(“AVG(age)”); // 子查询选择 AVG(age) LambdaQueryWrapperUser mainQuery new LambdaQueryWrapper(); mainQuery.gt(User::getAge, subQuery); // 这里直接传入 wrapper 对象 // 注意这种写法需要 MP 版本支持且语法可能随版本变化。更通用的做法是使用自定义SQL或多表查询。对于非常复杂的子查询或连表查询QueryWrapper可能力有不逮。这时我们有几种选择使用 MP 的Select注解或 XML 编写完整 SQL。使用 MP 的连表查询功能如join 但功能相对简单。在QueryWrapper中使用apply方法拼接自定义 SQL 片段。// 使用 apply 拼接自定义片段 LambdaQueryWrapperUser lqw new LambdaQueryWrapper(); lqw.apply(“dept_id IN (SELECT id FROM sys_dept WHERE dept_name {0})”, “研发部”);apply方法中的{0}是占位符会被后面的参数安全地替换预编译防注入。这种方式比inSql的纯字符串拼接更安全一些但依然是字符串操作。我的建议是对于简单的、单表的动态查询优先使用LambdaQueryWrapper。对于涉及多表关联、复杂聚合、数据库特定函数的查询评估后如果QueryWrapper写起来很别扭就果断使用 MyBatis XML 或注解方式。不要试图用QueryWrapper解决所有问题选择合适的工具才是高效的关键。4.3 查询结果控制选择字段与去重默认情况下selectList会查询所有字段SELECT *。但有时我们只需要部分字段或者需要去重。1. 指定查询字段使用select(String… columns)方法。// 只查询 id, username, email 字段 LambdaQueryWrapperUser lqw new LambdaQueryWrapper(); lqw.select(User::getId, User::getUsername, User::getEmail); // 对应的SQL: SELECT id, username, email FROM sys_user2. 排除查询字段使用select(ClassT entityClass, PredicateTableFieldInfo predicate)这个 lambda 方法MP 3.4.0 特性更直观。或者更早版本可以结合select排除。// 查询除 create_time 和 update_time 外的所有字段 (Lambda方式需要MP版本支持) // 一种常见做法是明确指定需要的字段而不是排除。3. 查询去重使用select(“DISTINCT column1, column2”)。注意这里传入的是字符串。// 查询不重复的部门ID LambdaQueryWrapperUser lqw new LambdaQueryWrapper(); lqw.select(“DISTINCT dept_id”); ListUser users userMapper.selectList(lqw); // 返回的User对象只有deptId字段有值4. 聚合查询虽然QueryWrapper主要用于构建 WHERE 条件但也可以通过select和groupBy进行简单的聚合查询结果需要自定义返回对象。// 查询每个部门的用户数量 LambdaQueryWrapperUser lqw new LambdaQueryWrapper(); lqw.select(“dept_id, COUNT(*) as user_count”) .groupBy(“dept_id”); // 注意返回的 ListUser 无法直接映射 count需要定义 ResultMap 或使用 Map 接收 ListMapString, Object maps userMapper.selectMaps(lqw); for (MapString, Object map : maps) { Long deptId (Long) map.get(“dept_id”); Long count (Long) map.get(“user_count”); System.out.println(“部门:” deptId “, 人数:” count); }这里使用了selectMaps方法它返回一个ListMapString, ObjectMap 的 key 是查询的字段名或别名value 是查询结果。这对于动态字段或聚合查询非常有用。5. 常见问题与排查技巧实录即使熟练使用QueryWrapper在实际开发中还是会遇到一些坑。下面是我总结的常见问题及解决方案。5.1 空值条件处理不当导致查询错误这是最常见的问题之一。问题场景1in方法传入空列表。ListLong idList getIdsFromSomewhere(); // 可能返回空列表 queryWrapper.in(“id”, idList); // 如果 idList 为空生成的SQL是WHERE id IN ()语法错误解决方案if (CollectionUtils.isNotEmpty(idList)) { queryWrapper.in(“id”, idList); } // 或者使用条件参数 queryWrapper.in(CollectionUtils.isNotEmpty(idList), “id”, idList);问题场景2字符串模糊查询传入空字符串或全是空格。String keyword request.getParameter(“keyword”); // 可能是 “” queryWrapper.like(“title”, keyword); // 生成WHERE title LIKE ‘%%’会匹配所有记录可能不是预期行为。解决方案if (StringUtils.isNotBlank(keyword)) { // 使用 isNotBlank 排除 null、空串和纯空格 queryWrapper.like(“title”, keyword.trim()); } // 或使用条件参数 queryWrapper.like(StringUtils.isNotBlank(keyword), “title”, keyword ! null ? keyword.trim() : null);5.2 条件逻辑错误AND 和 OR 优先级混淆SQL 中 AND 的优先级高于 OR。在QueryWrapper中构造复杂条件时如果不加括号很容易出错。错误示例想查询 (状态为1 或 状态为2) 并且 名字包含“张” 的用户。queryWrapper.eq(“status”, 1) .or() .eq(“status”, 2) .like(“name”, “张”); // 生成的SQL: WHERE status 1 OR status 2 AND name LIKE ‘%张%’ // 实际执行顺序 WHERE status 1 OR (status 2 AND name LIKE ‘%张%’) // 这会把状态为1的所有用户都查出来不管名字是什么逻辑错误正确写法使用nested或or(Consumer)来加括号。// 写法一使用 nested queryWrapper.nested(i - i.eq(“status”, 1).or().eq(“status”, 2)) .like(“name”, “张”); // 写法二使用 and 嵌套 or queryWrapper.and(i - i.eq(“status”, 1).or().eq(“status”, 2)) .like(“name”, “张”); // 写法三使用 or 的 Consumer 形式需要调整结构 queryWrapper.like(“name”, “张”) .and(i - i.eq(“status”, 1).or().eq(“status”, 2)); // 生成的SQL都是WHERE (status 1 OR status 2) AND name LIKE ‘%张%’排查技巧当你觉得查询结果不对时第一件事就是打开 MP 的 SQL 日志看看实际生成的 SQL 语句是什么。在application.yml中配置mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印完整SQL含参数对比生成的 SQL 和你心中预期的 SQL就能快速定位是条件逻辑问题还是其他问题。5.3 字段名映射问题数据库字段 vs 实体属性QueryWrapper默认使用的是数据库的列名column而LambdaQueryWrapper使用的是实体类的属性名通过方法引用获取对应的列名。如果数据库字段命名风格下划线user_name和实体属性命名风格驼峰userName不一致就需要小心。使用字符串的QueryWrapper必须传入数据库的列名。// 假设数据库字段是 user_name实体属性是 userName QueryWrapperUser qw new QueryWrapper(); qw.eq(“user_name”, “张三”); // 正确 qw.eq(“userName”, “张三”); // 错误会报错找不到列 ‘userName’使用LambdaQueryWrapper传入实体类的属性引用即可MP 会自动根据你的配置默认是下划线转驼峰进行转换。LambdaQueryWrapperUser lqw new LambdaQueryWrapper(); lqw.eq(User::getUserName, “张三”); // 正确。MP会自动将 ‘userName’ 转换为 ‘user_name’这是强烈推荐使用LambdaQueryWrapper的另一个重要原因它避免了硬编码字符串列名减少了因列名错误导致的 Bug。常见错误在QueryWrapper中误用了实体属性名或者在LambdaQueryWrapper中引用了不存在的属性编译会报错。如果全局配置了table-underline: false关闭了下划线转换那么LambdaQueryWrapper也会按属性名直接映射需要确保实体属性名和数据库列名完全一致。5.4 性能问题索引失效与大数据量查询QueryWrapper只是帮你生成 SQL生成的 SQL 质量直接影响性能。1. 模糊查询导致索引失效LIKE ‘%value%’这种前后都模糊的查询在大多数数据库上会导致该字段的索引失效。如果该字段查询频繁且数据量大需要考虑使用搜索引擎如 Elasticsearch或调整查询模式如只使用右模糊LIKE ‘value%’。2. 对 NULL 值使用!判断queryWrapper.ne(“column”, value)在value为null时生成的 SQL 是column NULL。在 SQL 中任何与NULL的比较包括结果都是UNKNOWN不会返回任何行。如果你真想查询非 NULL 值应该用isNotNull(“column”)。3. 分页查询深翻页问题使用Page对象进行selectPage查询时在大数据量下比如第 10000 页LIMIT 100000, 10这种写法会非常慢因为数据库需要先扫描并丢弃前面的 100000 条记录。解决方案是使用“上一页最大ID”查询法或者一些数据库的优化特性如 MySQL 8.0 的ROW_NUMBER()窗口函数。4. 查询字段过多默认select *会查询所有字段包括大文本字段如content。如果不需要一定要用select方法指定需要的字段减少网络传输和内存占用。5.5 与 XML 自定义 SQL 的协同工作有时我们需要在 XML 中写复杂的 SQL但又想复用QueryWrapper的动态条件构造能力。MP 提供了完美的支持。在 Mapper 接口中定义方法public interface UserMapper extends BaseMapperUser { // 使用 Param(“ew”) 注解并将 Wrapper 作为参数传入 ListUser selectComplexList(Param(“ew”) QueryWrapperUser wrapper); }在对应的 XML 映射文件中select id“selectComplexList” resultType“User” SELECT u.*, d.dept_name FROM sys_user u LEFT JOIN sys_dept d ON u.dept_id d.id ${ew.customSqlSegment} !-- 关键这里会插入 QueryWrapper 生成的 WHERE 条件部分 -- /select注意${ew.customSqlSegment}这个语法。它会把QueryWrapper生成的WHERE 关键字之后的条件字符串不包括 WHERE 本身插入进来。这样你既可以在 Java 代码里用QueryWrapper灵活构造条件又可以在 XML 里写复杂的 SELECT 和 JOIN 部分两者结合威力巨大。最后一个小技巧如果你想在自定义 SQL 中使用QueryWrapper但不需要它自动添加 WHERE 关键字比如你的 SQL 里已经有 WHERE 了可以在QueryWrapper构造完成后调用getCustomSqlSegment()方法获取纯条件字符串然后手动拼接。但更推荐使用$占位符的方式让 MP 自动处理。