十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MyBatis-Flex QueryWrapper:动态SQL构建利器与MyBatis-Plus对比

MyBatis-Flex QueryWrapper:动态SQL构建利器与MyBatis-Plus对比 1. 项目概述为什么我们需要一个更“灵活”的查询包装器在Java后端开发尤其是基于Spring Boot和MyBatis的生态里与数据库打交道是家常便饭。早期我们写SQL后来用MyBatis Generator生成一堆XML和Mapper接口再后来MyBatis-PlusMP的出现用QueryWrapper这类链式API把动态SQL的构建变得像搭积木一样直观极大地提升了开发效率。那么为什么在已经有了MP这样成熟方案的今天MyBatis-Flex以下简称Flex还要推出自己的QueryWrapper呢这绝不是简单的重复造轮子。我最初接触Flex的QueryWrapper是因为一个老项目的迁移需求。那个项目里存在大量复杂的、动态条件拼接的查询用原生MyBatis的XML写起来冗长且难以维护用MP的Wrapper在某些极端的多表关联和子查询场景下又感觉语法有些“绕”或者性能上存在优化空间。Flex的QueryWrapper给我的第一印象是它试图在MP的便捷性与原生SQL的表达力之间找到一个更精准的平衡点。它不仅仅是一个条件构造器更是一个贯穿Flex整个ORM理念的核心组件强调“Flex”ibility——灵活性。简单来说MyBatis-Flex的QueryWrapper是一个用于动态构建SQL查询条件的强大工具。它通过一套流畅的API让你能用Java代码以类型安全、IDE友好有代码提示的方式构造出从简单到极其复杂的SQL WHERE、HAVING、ORDER BY、GROUP BY等子句。其核心价值在于告别字符串拼接防SQL注入告别XML中大量的if test...标签让查询逻辑像业务代码一样清晰、可复用、易测试。无论是简单的id 1还是涉及多表联动、嵌套子查询、动态选择字段的复杂查询都能优雅地应对。2. 核心设计哲学与MyBatis-Plus的QueryWrapper有何不同很多从MP转过来的开发者会第一个问这个问题。虽然两者都叫QueryWrapper目标也相似但设计哲学和实现细节上有显著区别理解了这些你才能用好Flex的Wrapper。2.1 核心差异实体与条件的分离度MP的Wrapper与Entity绑定非常紧密。你通常这样用new QueryWrapperUser().eq(name, 张三)。这里的name是数据库字段名字符串MP后期版本也支持LambdaQueryWrapper用User::getName方法引用。但其设计核心是围绕一个主实体进行的。Flex的QueryWrapper在理念上更“独立”。它虽然也完美支持基于实体类的Lambda表达式功能更强但其设计初衷是作为一个独立的、不依赖特定实体的查询描述器。你可以先构建一个复杂的Wrapper然后再将其应用于任何一个Mapper或DAO方法。这种解耦带来了更大的灵活性特别是在处理涉及多个不直接映射到单一实体的复杂查询时。2.2 链式API的“纯净性”与功能专注性MP的QueryWrapper继承自AbstractWrapper集成了条件eq,like、选择字段select、排序orderBy、分组groupBy甚至分页last等众多方法。它是一个功能大集合。Flex的QueryWrapper在链式调用上更强调“阶段清晰”。它的核心专注于WHERE条件的构建。虽然它也支持select和orderBy等但你会发现在Flex中select查询字段和from表通常是在执行查询时与Mapper方法或Db工具类结合来完成的。例如你构建一个条件Wrapper然后通过userMapper.selectListByQuery(wrapper)来执行或者在select时指定字段。这种设计让QueryWrapper的职责更单一也避免了MP中偶尔出现的“select被意外覆盖”或链式调用顺序导致歧义的问题。2.3 对复杂SQL的原生支持深度这是Flex的QueryWrapper的一大亮点。它对子查询、Union查询、With语句CTE的支持是内建且语法非常直观的。在MP中实现一个子查询作为条件可能需要借助apply或inSql拼接字符串或者使用比较新的exists方法但灵活性和类型安全稍弱。而在Flex中你可以直接在一个QueryWrapper中嵌套另一个QueryWrapper代码看起来就像SQL的逻辑一样自然。// 假设查询年龄大于平均年龄的用户 QueryWrapper subQuery QueryWrapper.create() .select(AVG(age)) .from(Account.class); QueryWrapper mainQuery QueryWrapper.create() .select() .from(Account.class) .where(Account::getAge).gt(subQuery);这种“Wrapper嵌套Wrapper”的方式让构建复杂查询的思维负担大大降低。3. 基础入门从零开始构建你的第一个QueryWrapper让我们暂时忘掉那些复杂特性先看看如何用Flex的QueryWrapper完成日常80%的查询工作。假设我们有一个Account实体类对应数据库account表字段有id,user_name,age,birthday等。3.1 创建Wrapper的几种方式最常用的是静态工厂方法QueryWrapper.create()它创建了一个空的条件包装器。QueryWrapper queryWrapper QueryWrapper.create();如果你已经有了一个实体对象想以其字段值作为等值查询条件可以Account account new Account(); account.setUserName(张三); account.setAge(25); // 创建Wrapper并自动添加 entity 中非null字段的等值条件 QueryWrapper queryWrapper QueryWrapper.create(account); // 生成的SQL近似WHERE user_name 张三 AND age 25注意QueryWrapper.create(entity)这种方式会默认将实体中所有非null属性作为eq条件。对于数值型字段如果默认值是0也会被加入条件这可能不是你想要的行为。通常更推荐使用清晰的条件API手动构建或者使用QueryWrapper.create()后链式添加条件。3.2 基础条件构造eq, ne, gt, lt, like...这些方法的使用非常直观支持两种主要形式基于字符串字段名和基于Lambda方法引用。强烈推荐使用Lambda形式它能获得编译时类型检查和IDE的代码补全重构也更安全。Lambda形式推荐QueryWrapper query QueryWrapper.create() .select() .from(Account.class) .where(Account::getUserName).eq(张三) .and(Account::getAge).ge(18) // 大于等于 .and(Account::getBirthday).lt(LocalDate.now()); // 小于当前日期字符串形式需谨慎QueryWrapper query QueryWrapper.create() .where(user_name ?, 张三) // 直接写SQL片段参数用占位符 .and(age ?, 18); // 或者使用条件方法 query.eq(user_name, 张三).ge(age, 18);字符串形式在动态表名、动态字段名等极端场景下可能有用但牺牲了类型安全和重构便利性是SQL注入的潜在风险点尽管Flex内部会做参数化处理但拼接逻辑复杂时容易出错。3.3 逻辑组合and, or 的灵活运用Flex的QueryWrapper在逻辑组合上非常强大和清晰。and()和or()方法不仅用于连接条件更重要的是用于构建逻辑分组。场景1简单的 AND/OR 混合// 查询姓名为张三 或者 (年龄大于18且姓名不为空的用户) QueryWrapper query QueryWrapper.create() .from(Account.class) .where(Account::getUserName).eq(张三) .or(w - w.gt(Account::getAge, 18).isNotNull(Account::getUserName)); // 生成的SQL: WHERE user_name 张三 OR (age 18 AND user_name IS NOT NULL)这里的关键是.or(w - w...)它创建了一个新的匿名QueryWrapper作为or的子条件组。and同理。场景2复杂的多级嵌套// 查询 (状态为激活 且 (是VIP或注册时间大于1年)) 或者 用户ID在指定列表里 QueryWrapper query QueryWrapper.create() .from(Account.class) .where(Account::getStatus).eq(1) .and(w1 - w1.eq(Account::getIsVip, 1) .or(w2 - w2.lt(Account::getRegisterTime, LocalDateTime.now().minusYears(1)))) .or(Account::getId).in(idList);这种Lambda嵌套的写法几乎就是SQL逻辑的直译可读性极强。4. 进阶技巧解锁复杂查询场景掌握了基础我们就可以挑战更复杂的业务查询了。这是FlexQueryWrapper真正发挥威力的地方。4.1 动态条件拼接告别if-else地狱这是QueryWrapper的看家本领。传统方式需要在XML里写一堆if或者在Java代码里拼接StringBuilder既难看又容易出错。现在可以这样public QueryWrapper buildAccountQuery(String userName, Integer minAge, Integer maxAge, Integer status) { return QueryWrapper.create() .from(Account.class) // 动态添加条件当参数不为null或满足其他业务逻辑时才添加 .when(StringUtils.hasText(userName), w - w.eq(Account::getUserName, userName)) .when(minAge ! null, w - w.ge(Account::getAge, minAge)) .when(maxAge ! null, w - w.le(Account::getAge, maxAge)) .when(status ! null, w - w.eq(Account::getStatus, status)) .orderBy(Account::getId, true); }when(boolean condition, ConsumerQueryWrapper action)方法极其优雅。当condition为true时执行action一个接收QueryWrapper的Lambda表达式向其中添加条件。整个构建过程是声明式的逻辑一目了然。4.2 子查询轻松应对“查询中的查询”子查询是复杂报表和数据分析的常客。Flex让子查询变得简单。作为条件值标量子查询// 查询销售额高于平均销售额的销售员 QueryWrapper avgQuery QueryWrapper.create() .select(AVG(sale_amount)) .from(SaleRecord.class); QueryWrapper mainQuery QueryWrapper.create() .from(Salesman.class) .where(Salesman::getSaleAmount).gt(avgQuery);作为IN/NOT IN的条件列子查询// 查询有订单的用户 QueryWrapper orderUserIdQuery QueryWrapper.create() .select(DISTINCT user_id) .from(Order.class); QueryWrapper userQuery QueryWrapper.create() .from(User.class) .where(User::getId).in(orderUserIdQuery);作为EXISTS/NOT EXISTS的条件存在性检查// 查询从未下过订单的用户 (NOT EXISTS) QueryWrapper existsQuery QueryWrapper.create() .from(Order.class) .where(Order::getUserId).eq(User::getId); // 关联条件 QueryWrapper userQuery QueryWrapper.create() .from(User.class) .where(existsQuery).notExists(); // 使用.notExists() // 或者 .where(existsQuery).exists() 表示存在订单的用户4.3 多表关联查询JoinFlex的QueryWrapper对多表查询的支持非常灵活。你可以使用left join,inner join等。// 查询用户及其订单数量 QueryWrapper query QueryWrapper.create() .select(u.id, u.user_name, count(o.id) as order_count) .from(User.class).as(u) .leftJoin(Order.class).as(o).on(u.id o.user_id) .groupBy(u.id, u.user_name) .having(count(o.id) ?, 0);注意这里on条件目前使用的是字符串。对于更类型安全的关联Flex推荐使用其Relations功能基于注解定义实体间关系或者使用QueryWrapper的where条件来模拟关联这取决于你的具体架构选择。4.4 选择特定字段与聚合函数你可以通过select方法精确控制查询的字段这对优化查询性能避免SELECT *和DTO投影非常有用。// 只查询需要的字段 QueryWrapper query QueryWrapper.create() .select(Account::getId, Account::getUserName, Account::getAge) .from(Account.class) .where(Account::getStatus).eq(1); // 使用聚合函数和别名 QueryWrapper statQuery QueryWrapper.create() .select(department_id, COUNT(*) as emp_count, AVG(salary) as avg_salary, MAX(salary) as max_salary) .from(Employee.class) .groupBy(Employee::getDepartmentId);5. 实战将对象动态转换为QueryWrapper“对象转querywrapper”是最近的一个热门讨论点。这通常指的是将一个前端传递过来的、包含多种可能过滤条件的查询参数对象比如UserQueryParam自动转换成一个QueryWrapper。Flex本身没有提供全自动的“对象映射Wrapper”功能像MP的Condition构造器那样但这恰恰给了我们更灵活、更可控的实现空间。5.1 手动构建转换器推荐这是最清晰、最可控的方式。在你的Service层创建一个方法专门负责转换。Data // 使用Lombok public class AccountQueryParam { private String userName; private Integer minAge; private Integer maxAge; private Integer status; private ListString userNames; private LocalDateTime createTimeStart; private LocalDateTime createTimeEnd; } public QueryWrapper toQueryWrapper(AccountQueryParam param) { QueryWrapper qw QueryWrapper.create(); if (StringUtils.hasText(param.getUserName())) { // 模糊查询 qw.and(Account::getUserName).like(param.getUserName()); } if (param.getMinAge() ! null) { qw.and(Account::getAge).ge(param.getMinAge()); } if (param.getMaxAge() ! null) { qw.and(Account::getAge).le(param.getMaxAge()); } if (param.getStatus() ! null) { qw.and(Account::getStatus).eq(param.getStatus()); } if (CollectionUtils.isNotEmpty(param.getUserNames())) { qw.and(Account::getUserName).in(param.getUserNames()); } // 时间范围查询 if (param.getCreateTimeStart() ! null param.getCreateTimeEnd() ! null) { qw.and(Account::getCreateTime).between(param.getCreateTimeStart(), param.getCreateTimeEnd()); } else if (param.getCreateTimeStart() ! null) { qw.and(Account::getCreateTime).ge(param.getCreateTimeStart()); } else if (param.getCreateTimeEnd() ! null) { qw.and(Account::getCreateTime).le(param.getCreateTimeEnd()); } // 默认排序 qw.orderBy(Account::getCreateTime, false); return qw; }这种方式虽然代码量稍多但优势巨大完全可控你可以精确决定每个字段的查询逻辑是eq还是like是ge还是gt。业务逻辑清晰转换逻辑集中在一处易于阅读、测试和维护。安全性高避免了反射或自动映射可能带来的意外行为或安全漏洞。5.2 利用when方法简化转换器结合when方法可以让上面的转换器代码更函数式、更简洁public QueryWrapper toQueryWrapperWithWhen(AccountQueryParam param) { return QueryWrapper.create() .when(StringUtils.hasText(param.getUserName()), w - w.like(Account::getUserName, param.getUserName())) .when(param.getMinAge() ! null, w - w.ge(Account::getAge, param.getMinAge())) .when(param.getMaxAge() ! null, w - w.le(Account::getAge, param.getMaxAge())) .when(param.getStatus() ! null, w - w.eq(Account::getStatus, param.getStatus())) .when(CollectionUtils.isNotEmpty(param.getUserNames()), w - w.in(Account::getUserName, param.getUserNames())) .when(param.getCreateTimeStart() ! null param.getCreateTimeEnd() ! null, w - w.between(Account::getCreateTime, param.getCreateTimeStart(), param.getCreateTimeEnd())) .when(param.getCreateTimeStart() ! null param.getCreateTimeEnd() null, w - w.ge(Account::getCreateTime, param.getCreateTimeStart())) .when(param.getCreateTimeStart() null param.getCreateTimeEnd() ! null, w - w.le(Account::getCreateTime, param.getCreateTimeEnd())) .orderBy(Account::getCreateTime, false); }5.3 谨慎使用反射或工具类自动转换网上有些方案通过反射遍历对象属性根据属性名和值自动生成eq条件。我个人不推荐在生产环境大规模使用这种方法原因如下缺乏灵活性所有字段只能是一种操作通常是eq无法满足like、between、gt等多样化的查询需求。类型处理复杂需要处理null值、空字符串、集合、日期范围等特殊情况代码会变得臃肿且脆弱。破坏封装将查询参数的内部结构字段名暴露给了底层的SQL构造逻辑耦合度高。性能开销反射调用有一定性能损耗虽然对于单个请求可忽略但设计上不优雅。如果确实有大量简单的等值查询场景可以编写一个受限的、安全的工具方法但务必明确其适用范围并做好文档说明。6. 性能优化与最佳实践用好QueryWrapper不仅能提升开发效率也能兼顾性能。6.1 避免在循环中创建Wrapper这是一个常见的性能陷阱。如果你需要在循环中根据不同的条件查询应该复用QueryWrapper的基础部分或者在循环内只修改参数值而不是每次都new QueryWrapper.create()。不佳实践ListAccount result new ArrayList(); for (Long id : idList) { QueryWrapper qw QueryWrapper.create().eq(Account::getId, id); // 每次循环都新建 result.addAll(mapper.selectListByQuery(qw)); }较佳实践使用in语句if (CollectionUtils.isNotEmpty(idList)) { QueryWrapper qw QueryWrapper.create().in(Account::getId, idList); ListAccount result mapper.selectListByQuery(qw); }如果业务逻辑必须循环查询考虑使用批量查询接口。6.2 注意索引命中QueryWrapper生成的SQL最终要交给数据库执行。要像关心手写SQL一样关心其生成的SQL是否会命中索引。对where条件中的字段尤其是用于等值查询、范围查询和排序的字段考虑建立合适的数据库索引。避免在索引列上使用函数操作例如.where(DATE(create_time) ?, someDate)这会导致索引失效。Flex提供了丰富的日期时间条件方法如.where(Account::getCreateTime).ge(...)会生成正确的SQL。使用explain命令分析复杂QueryWrapper生成的SQL执行计划。6.3 与分页插件搭配使用Flex有良好的分页支持。通常分页信息页码、每页条数不放在QueryWrapper里而是作为单独的参数与Wrapper一起传给Mapper或Db的查询方法。// 使用Page对象进行分页 PageAccount page new Page(1, 10); // 第一页每页10条 QueryWrapper qw QueryWrapper.create().eq(Account::getStatus, 1).orderBy(Account::getId); PageAccount pageResult accountMapper.paginate(page, qw); ListAccount records pageResult.getRecords(); long totalRow pageResult.getTotalRow();6.4 调试如何查看生成的SQL在开发阶段查看QueryWrapper最终生成的SQL及其参数非常重要。开启MyBatis日志在application.yml中设置logging.level.[你的Mapper包路径]DEBUG这是最直接的方式。使用Flex的SQL调试输出Flex配置中也可以开启SQL输出它能将参数替换到SQL中生成可直接拷贝到数据库客户端执行的完整SQL对调试复杂条件逻辑尤其有用。使用toSQL()方法QueryWrapper对象有一个toSQL()方法可以返回一个包含SQL语句和参数列表的对象方便在代码中直接打印检查。QueryWrapper qw QueryWrapper.create()...; QueryWrapper.ToSQL toSQL qw.toSQL(); System.out.println(SQL: toSQL.getSql()); System.out.println(Params: toSQL.getParamList());7. 常见问题与排查技巧实录在实际使用中你可能会遇到一些“坑”。这里记录了几个典型问题和我的解决思路。7.1 条件不生效检查逻辑运算符优先级和括号SQL中AND的优先级高于OR。在QueryWrapper中如果你没有显式地用or(w-w...)进行分组可能会得到意想不到的结果。问题示例// 意图查询 (状态为1 或 状态为2) 并且 姓名为张三 QueryWrapper qw QueryWrapper.create() .eq(Account::getStatus, 1) .or().eq(Account::getStatus, 2) .eq(Account::getUserName, 张三); // 实际生成的SQL: WHERE status 1 OR status 2 AND user_name 张三 // 等价于 status 1 OR (status 2 AND user_name 张三) 这不符合预期。正确写法QueryWrapper qw QueryWrapper.create() .and(w - w.eq(Account::getStatus, 1).or().eq(Account::getStatus, 2)) .eq(Account::getUserName, 张三); // 生成的SQL: WHERE (status 1 OR status 2) AND user_name 张三心得当条件中包含OR时一定要仔细考虑是否需要加“括号”在Flex中就是用and(w-w...)或or(w-w...)来创建逻辑分组。7.2 模糊查询like时通配符处理使用like方法时Flex默认不会在值两侧添加%通配符。你需要自己决定匹配模式。// 前缀匹配查找以“张”开头的名字 qw.like(Account::getUserName, 张%); // 或使用 likeRight 方法它自动在值右侧加 % qw.likeRight(Account::getUserName, 张); // 后缀匹配查找以“三”结尾的名字 qw.like(Account::getUserName, %三); // 或使用 likeLeft qw.likeLeft(Account::getUserName, 三); // 包含匹配查找名字中包含“三”的 qw.like(Account::getUserName, %三%); // 或直接使用 like但自己加% qw.like(Account::getUserName, % name %);注意如果用户输入可能包含通配符本身如%或_需要进行转义防止这些字符被SQL解析。Flex提供了escape方法或者更安全的做法是在业务层对输入进行清洗。7.3 处理NULL值isNull、isNotNull与eq的陷阱在SQL中column NULL这个条件永远为假必须用column IS NULL。QueryWrapper很好地处理了这一点。// 正确查询姓名为空的记录 qw.isNull(Account::getUserName); // 正确查询姓名不为空的记录 qw.isNotNull(Account::getUserName); // 危险如果你想查询年龄等于传入参数age的记录当age可能为null时 Integer age ...; // 可能为null // 错误如果age为null生成的SQL是 age null查不到数据 // qw.eq(Account::getAge, age); // 正确做法动态判断 if (age ! null) { qw.eq(Account::getAge, age); } else { // 如果业务上“age为null”也是一种查询条件应该用 isNull qw.isNull(Account::getAge); }7.4 日期时间范围查询的边界问题查询“某一天”的数据是一个高频需求。很多人会犯一个错误用create_time 2023-10-27。如果create_time是datetime或timestamp类型这很可能查不到数据因为时间部分不匹配。正确做法是使用范围查询LocalDate queryDate LocalDate.of(2023, 10, 27); LocalDateTime startOfDay queryDate.atStartOfDay(); // 2023-10-27 00:00:00 LocalDateTime endOfDay queryDate.plusDays(1).atStartOfDay(); // 2023-10-28 00:00:00 QueryWrapper qw QueryWrapper.create() .ge(Account::getCreateTime, startOfDay) .lt(Account::getCreateTime, endOfDay); // 注意是小于 endOfDay 不是小于等于 // 生成的SQL: create_time 2023-10-27 00:00:00 AND create_time 2023-10-28 00:00:00这里使用lt小于而不是le小于等于是关键它确保了包含2023-10-27 23:59:59.999的所有记录又不会包含2023-10-28 00:00:00的记录。7.5 联表查询时字段别名冲突在多表join查询并使用select指定字段时如果两个表有同名字段必须在SQL中使用别名区分否则结果映射会出错。QueryWrapper query QueryWrapper.create() .select(u.id as userId, u.name as userName, d.id as deptId, d.name as deptName) .from(User.class).as(u) .leftJoin(Department.class).as(d).on(u.dept_id d.id);在定义返回的结果类型可以是实体、DTO或Map时需要确保属性名与这些别名对应。我个人在实际项目中对于复杂的多表查询更倾向于使用Flex的多表查询APT功能。它通过注解处理器在编译时生成强类型的查询辅助类能提供完全类型安全的联表查询体验彻底避免字段别名错误和SQL字符串硬编码的问题虽然学习成本稍高但对于大型项目来说是值得的投资。最后MyBatis-Flex的QueryWrapper是一个需要稍加练习才能完全掌握其精妙的工具。开始时可能会觉得有些概念与MP不同但一旦适应了其“独立”、“清晰”、“强大”的设计风格尤其是在处理复杂动态查询时你会感受到它带来的那种顺畅和掌控感。我的建议是从一个新模块或一个小型项目开始尝试先从基础的eq、like、when用起逐步尝试子查询和复杂组合很快你就能把它变成你数据库查询的利器。
返回列表