在MyBatis Plus的日常开发中,QueryWrapper是我们最常用的查询条件构造器之一。但很多开发者在使用ne(不等于)条件时,都会遇到一个典型的陷阱:当字段值为NULL时,查询结果与预期不符。这个问题看似简单,实则涉及到SQL的三值逻辑和MyBatis Plus的查询机制。
让我们先还原问题现场。假设我们有一个电表读数表(read_meter),其中remark字段可能为NULL。现在需要查询所有remark不等于"本次报停结束"的记录,包括remark为NULL的记录。新手开发者通常会这样写:
java复制QueryWrapper<ReadMeter> wrapper = new QueryWrapper<>();
wrapper.ne("remark", "本次报停结束");
但实际执行时,会发现结果集中不包含remark为NULL的记录。这是因为在SQL的三值逻辑中,NULL与任何值的比较(包括不等于)都会返回UNKNOWN,而非TRUE。
SQL中的逻辑判断与Java等编程语言不同,它采用三值逻辑:
当执行remark != '本次报停结束'时:
而SQL的WHERE子句只会返回条件为TRUE的记录,UNKNOWN和FALSE都会被过滤掉。这就是为什么NULL值记录"消失"的根本原因。
正确的做法是显式处理NULL值情况,使用逻辑或(OR)将NULL值包含进来:
java复制QueryWrapper<ReadMeter> wrapper = new QueryWrapper<>();
wrapper.and(w -> w.isNull("remark").or().ne("remark", "本次报停结束"));
这段代码生成的SQL相当于:
sql复制WHERE (remark IS NULL OR remark != '本次报停结束')
让我们拆解这个解决方案:
and()方法接受一个Consumer函数,用于嵌套条件isNull("remark")生成remark IS NULL条件or()表示逻辑或ne("remark", "本次报停结束")生成remark != '本次报停结束'这种写法确保了:
除了上述标准方案,还有几种等效写法:
写法一:使用OR连接
java复制wrapper.or(w -> w.isNull("remark"))
.or(w -> w.ne("remark", "本次报停结束"));
写法二:使用apply方法(原生SQL片段)
java复制wrapper.apply("remark IS NULL OR remark != {0}", "本次报停结束");
提示:虽然apply方法更灵活,但建议优先使用类型安全的条件构造器,避免SQL注入风险。
当处理大量数据时,OR条件可能会导致索引失效。对于remark字段:
如果remark字段有索引:
ne条件通常无法使用索引isNull条件可以使用索引(如果数据库支持)优化建议:
为了避免团队中每个开发者都踩这个坑,建议:
针对NULL值查询,测试用例应该包含:
java复制@Test
public void testQueryWithNullRemark() {
// 准备测试数据
ReadMeter m1 = new ReadMeter().setRemark(null);
ReadMeter m2 = new ReadMeter().setRemark("本次报停结束");
ReadMeter m3 = new ReadMeter().setRemark("其他备注");
// 执行查询
List<ReadMeter> result = repository.list(
new QueryWrapper<ReadMeter>()
.and(w -> w.isNull("remark").or().ne("remark", "本次报停结束"))
);
// 验证结果
assertThat(result).containsExactlyInAnyOrder(m1, m3);
assertThat(result).doesNotContain(m2);
}
MyBatis Plus的条件构造器在处理NULL值时遵循以下规则:
eq(null):会生成IS NULL条件ne(null):会生成IS NOT NULL条件但正如我们所见,ne("value")不会自动包含NULL值,这是SQL标准行为,不是MyBatis Plus的bug。
使用LambdaQueryWrapper可以让代码更类型安全:
java复制LambdaQueryWrapper<ReadMeter> wrapper = new LambdaQueryWrapper<>();
wrapper.and(w -> w.isNull(ReadMeter::getRemark)
.or().ne(ReadMeter::getRemark, "本次报停结束"));
Lambda方式的好处:
对于更复杂的条件组合,例如需要同时满足多个条件,但其中某个字段可能为NULL:
java复制wrapper.eq(ReadMeter::getMeterId, meterId)
.eq(ReadMeter::getMeterNumber, meterNumber)
.eq(ReadMeter::getReadType, 1)
.and(w -> w.isNull(ReadMeter::getRemark)
.or().ne(ReadMeter::getRemark, "本次报停结束"));
这种结构清晰地表达了业务逻辑:先匹配几个确定字段,再处理可能为NULL的remark字段。
不同数据库对NULL值的处理略有差异:
MySQL严格遵循SQL标准的三值逻辑,行为如前面所述。
Oracle也遵循三值逻辑,但有一些特殊函数:
NVL(expr1, expr2):如果expr1为NULL则返回expr2NULLIF(expr1, expr2):如果expr1等于expr2则返回NULLPostgreSQL提供了IS DISTINCT FROM语法,可以简化NULL值比较:
sql复制WHERE remark IS DISTINCT FROM '本次报停结束'
这个条件会自动包含NULL值,等同于:
sql复制WHERE remark IS NULL OR remark != '本次报停结束'
注意:MyBatis Plus目前没有直接支持
IS DISTINCT FROM语法,需要使用apply方法。
经过上述分析,我们可以总结出MyBatis Plus中处理NULL值的最佳实践:
isNull().or().ne()模式在实际项目中,我通常会创建一个工具类来封装这种常见的NULL值处理模式:
java复制public class QueryWrapperHelper {
public static <T> Consumer<AbstractWrapper<T, ?, ?>> neOrNull(String column, Object value) {
return w -> w.isNull(column).or().ne(column, value);
}
public static <T, R> Consumer<AbstractLambdaWrapper<T, ?>> neOrNull(SFunction<T, R> column, R value) {
return w -> w.isNull(column).or().ne(column, value);
}
}
// 使用示例
wrapper.and(QueryWrapperHelper.neOrNull(ReadMeter::getRemark, "本次报停结束"));
这种封装不仅提高了代码复用性,也使业务代码更加清晰易懂。