1. MyBatis-Plus比较运算符的核心价值解析
在数据库操作中,条件查询占据了日常开发的半壁江山。MyBatis-Plus作为MyBatis的增强工具包,其比较运算符的封装让动态条件构建变得异常简单。不同于传统XML中需要手写<if>标签的方式,MP通过链式调用的比较方法,让SQL条件像拼积木一样直观。
举个例子,当我们需要查询年龄大于18岁的用户时,传统MyBatis需要这样写:
xml复制<where>
<if test="age != null">
age > #{age}
</if>
</where>
而使用MyBatis-Plus的Lambda表达式只需:
java复制.lt(User::getAge, 18)
这种写法不仅减少了模板代码,更重要的是通过方法引用的方式实现了编译期类型安全。当实体类字段名变更时,IDE会立即提示错误,避免了运行时才发现SQL拼写错误的尴尬。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 六大核心比较运算符详解
2.1 等于与不等于运算
eq()和ne()分别对应SQL中的=和<>运算符。它们是最基础的比较操作,但有些细节值得注意:
java复制// 精确匹配用户名
queryWrapper.eq("username", "john_doe");
// 使用Lambda避免硬编码字段名
lambdaQueryWrapper.eq(User::getUsername, "john_doe");
// 不等于查询
queryWrapper.ne("status", 0);
注意:对于字符串类型的字段,eq()默认执行的是精确匹配。如果需要模糊查询,应该使用like()系列方法而非eq()。
2.2 范围比较运算符
gt()(大于)、ge()(大于等于)、lt()(小于)、le()(小于等于)这组方法构成了范围查询的基础:
java复制// 查询18岁以上的用户
lambdaQueryWrapper.gt(User::getAge, 18);
// 查询2023年之前的订单
lambdaQueryWrapper.lt(Order::getCreateTime, LocalDate.of(2023,1,1));
// 区间查询:分数在60到80之间的学生
lambdaQueryWrapper.ge(Student::getScore, 60)
.le(Student::getScore, 80);
实际开发中,处理日期范围查询时需要注意时区问题。建议在实体类中使用LocalDateTime而非Date,并在数据库连接参数中明确指定时区。
2.3 模糊匹配三剑客
like()、notLike()、likeLeft()、likeRight()提供了灵活的模糊查询能力:
java复制// 包含"技术"的标题
lambdaQueryWrapper.like(Article::getTitle, "技术");
// 以"张"开头的姓名
lambdaQueryWrapper.likeRight(User::getName, "张");
// 不以".com"结尾的邮箱
lambdaQueryWrapper.notLike(User::getEmail, ".com");
模糊查询的性能陷阱需要警惕:前导通配符(如%xxx)会导致索引失效。我曾在一个用户量超百万的项目中,因为不当使用like("%xxx")导致全表扫描,查询耗时从毫秒级暴增到秒级。正确的做法是尽量使用likeRight("xxx%")或考虑专门的全文检索方案。
3. 特殊场景下的运算符妙用
3.1 空值处理的艺术
isNull()和isNotNull()用于处理字段空值判断,但实际使用中有几个坑需要注意:
java复制// 查询未设置手机号的用户
lambdaQueryWrapper.isNull(User::getMobile);
// 查询已分配部门的员工
lambdaQueryWrapper.isNotNull(Employee::getDeptId);
这里有个容易混淆的点:数据库中的NULL与空字符串是不同的概念。isNull()判断的是真正的NULL值,而非空字符串。如果字段可能同时存在NULL和"",需要组合条件:
java复制// 查询既不是NULL也不是空字符串的记录
lambdaQueryWrapper.isNotNull(User::getAddress)
.ne(User::getAddress, "");
3.2 IN和NOT IN的批量操作
in()和notIn()在处理多值匹配时非常高效:
java复制// 查询特定ID集的用户
List<Long> ids = Arrays.asList(1L, 3L, 5L);
lambdaQueryWrapper.in(User::getId, ids);
// 排除特定状态的订单
lambdaQueryWrapper.notIn(Order::getStatus,
OrderStatus.CANCELLED, OrderStatus.REFUNDED);
当IN列表参数过多时(比如超过1000个),某些数据库(如Oracle)会报错。解决方案是分批查询或改用临时表关联。我在处理电商平台的SKU查询时,就遇到过IN列表超限的问题,最终采用分页批处理的方式解决。
3.3 BETWEEN的日期处理
虽然MP没有专门的between方法,但可以通过ge()和le()组合实现:
java复制// 查询2023年第一季度的订单
LocalDate start = LocalDate.of(2023,1,1);
LocalDate end = LocalDate.of(2023,3,31);
lambdaQueryWrapper.ge(Order::getCreateDate, start)
.le(Order::getCreateDate, end);
日期范围查询有个常见陷阱:如果字段包含时间部分,结束日期应该用2023-03-31 23:59:59而非2023-03-31 00:00:00,否则会漏掉3月31日当天的数据。建议使用LocalDateTime的atTime()方法:
java复制end.atTime(23, 59, 59)
4. 动态条件构建实战技巧
4.1 条件优先级控制
多个条件组合时,括号优先级会影响查询结果。MP通过and()和or()方法控制逻辑关系:
java复制// 查询(18岁以上或VIP)且状态正常的用户
lambdaQueryWrapper.and(wq -> wq
.gt(User::getAge, 18)
.or()
.eq(User::getVip, true))
.eq(User::getStatus, 1);
对应的SQL是:
sql复制WHERE (age > 18 OR vip = 1) AND status = 1
复杂条件嵌套时,建议像上面这样使用Lambda表达式保持代码可读性。我曾见过一个同事写的十几层嵌套的条件构造器,调试起来简直是噩梦。
4.2 动态条件组装
实际业务中经常需要根据参数动态构建查询条件:
java复制public Page<User> queryUsers(UserQuery query) {
return lambdaQuery()
.eq(query.getId() != null, User::getId, query.getId())
.like(StringUtils.isNotBlank(query.getName()),
User::getName, query.getName())
.gt(query.getMinAge() != null,
User::getAge, query.getMinAge())
.page(query.toPage());
}
这种写法利用了条件方法的第一个布尔参数,当为false时会忽略该条件。比起在业务代码中写if判断,这种方式更加优雅。
4.3 自定义SQL片段
对于特别复杂的条件,可以使用apply()方法插入原生SQL片段:
java复制// 查询距离某坐标10公里内的店铺
lambdaQueryWrapper.apply("ST_Distance(location, POINT({0}, {1})) <= {2}",
lon, lat, distance);
但要注意SQL注入风险,永远不要直接拼接用户输入:
java复制// 危险!可能引发SQL注入
wrapper.apply("create_time > '" + userInput + "'");
// 安全写法
wrapper.apply("create_time > {0}", userInput);
5. 性能优化与避坑指南
5.1 索引命中规则
不是所有比较操作都能利用索引:
- 能命中索引的操作:
eq(),in(),gt()/lt()(字段有索引时) - 通常不能命中索引的操作:
notLike(),ne(), 左侧使用函数的条件
一个真实案例:某用户表在email字段上有索引,但查询email like '%@gmail.com'仍然很慢。改为email like '%@gmail.com'并添加反向索引后,性能提升百倍。
5.2 大表查询优化
当表数据量超过百万时,比较操作需要特别注意:
- 避免全表扫描:确保WHERE条件能命中索引
- 分页优化:不要使用
count(1)查询总数,考虑游标分页 - 延迟关联:先查ID再关联详细信息
java复制// 不好的做法:直接大表分页
page(page, queryWrapper);
// 更好的做法:两阶段查询
Page<Long> idPage = page(new Page<>(1, 10),
queryWrapper.select("id"));
List<User> users = listByIds(idPage.getRecords());
5.3 逻辑删除的陷阱
如果启用了MP的逻辑删除功能(@TableLogic),所有查询会自动加上删除条件。但有些场景需要注意:
java复制// 即使使用eq(ID),实际SQL会变成:
// WHERE id = ? AND deleted = 0
lambdaQueryWrapper.eq(User::getId, 1);
// 如果需要查询包含已删除的记录
lambdaQueryWrapper.ignoreLogicDelete();
在关联查询时,逻辑删除可能导致意外结果。比如用户和订单关联查询,如果只对用户表忽略逻辑删除,订单表仍然会过滤已删除记录。这种情况下需要明确指定:
java复制wrapper.eq(User::getId, 1)
.ignoreLogicDelete() // 仅对主表生效
.eq(Order::getStatus, 1);
6. 与其它特性的组合使用
6.1 条件构造器与分页
分页查询时,比较运算符常与排序结合使用:
java复制// 按年龄降序分页查询成年人
lambdaQueryWrapper.gt(User::getAge, 18)
.orderByDesc(User::getAge)
.page(new Page<>(1, 10));
注意:当使用group by时,某些数据库(如MySQL)的分页结果可能不准确。解决方案是先查询ID再获取详细信息。
6.2 与Select语句的配合
比较运算符常与字段选择一起使用,避免查询不必要的数据:
java复制// 只查询用户名和邮箱
lambdaQueryWrapper.gt(User::getAge, 18)
.select(User::getUsername, User::getEmail);
对于大字段(如文本内容、图片地址),建议默认排除,按需查询:
java复制// 排除content大字段
lambdaQueryWrapper.select(User.class,
info -> !info.getColumn().equals("content"));
6.3 与Update结合的条件更新
比较运算符也可用于更新操作的条件限制:
java复制// 只更新特定条件的记录
lambdaUpdateWrapper.gt(User::getAge, 60)
.set(User::getDiscount, 0.8);
这种条件更新是原子操作,比"先查询再更新"更高效且避免并发问题。在库存扣减等场景特别有用。
7. 实际业务场景案例
7.1 电商商品筛选
典型的商品筛选界面往往包含多重条件:
java复制public Page<Product> searchProducts(ProductQuery query) {
return lambdaQuery()
.eq(query.getCategoryId() != null,
Product::getCategoryId, query.getCategoryId())
.between(query.getPriceRange() != null,
Product::getPrice,
query.getPriceMin(), query.getPriceMax())
.in(!CollectionUtils.isEmpty(query.getBrandIds()),
Product::getBrandId, query.getBrandIds())
.eq(Product::getStatus, ProductStatus.ON_SHELF)
.orderBy(query.getSortType() != null,
query.isAsc(), getSortField(query.getSortType()))
.page(query.toPage());
}
这种动态条件构造可以完美支持前端各种筛选组合,而无需写大量if-else。
7.2 后台审计日志查询
管理后台的日志查询通常需要灵活的时间范围和多条件过滤:
java复制public Page<AuditLog> queryLogs(AuditLogQuery query) {
return lambdaQuery()
.eq(StringUtils.isNotBlank(query.getOperator()),
AuditLog::getOperator, query.getOperator())
.ge(query.getStartTime() != null,
AuditLog::getOperationTime, query.getStartTime())
.le(query.getEndTime() != null,
AuditLog::getOperationTime,
query.getEndTime().plusDays(1).atStartOfDay())
.like(StringUtils.isNotBlank(query.getAction()),
AuditLog::getAction, query.getAction())
.orderByDesc(AuditLog::getOperationTime)
.page(query.toPage());
}
这里特别注意结束时间的处理:查询"2023-01-01至2023-01-31"的数据时,应该将结束时间扩展为"2023-02-01 00:00:00"。
7.3 动态报表数据统计
统计查询经常需要根据不同的维度进行分组和条件过滤:
java复制public List<SalesStats> getSalesStats(StatsQuery query) {
return query()
.select("product_category", "SUM(amount) as total")
.gt(query.getStartDate() != null,
"order_date", query.getStartDate())
.lt(query.getEndDate() != null,
"order_date", query.getEndDate())
.eq(query.getRegionId() != null,
"region_id", query.getRegionId())
.groupBy("product_category")
.orderByDesc("total")
.list();
}
这种灵活的统计查询可以避免为每个报表单独写SQL,提高代码复用率。
