最近在项目里用MyBatis写动态SQL时,突然遇到个让人头皮发麻的报错:"invalid comparison: java.util.Date and java.lang.String"。这个错误来得莫名其妙,明明代码看起来很正常,怎么就突然抛异常了呢?相信不少朋友在判断日期字段是否为空时,都写过类似这样的代码:
xml复制<if test="createTime != null and createTime != ''">
AND create_time = #{createTime}
</if>
表面上看这个判断逻辑很合理:既要检查日期不为null,又要防止空字符串。但实际运行时,MyBatis会直接抛出类型比较异常。这是因为在OGNL表达式里,Date对象和String对象根本就不是一个世界的生物,强行比较就像让猫和狗用同一种语言交流,结果必然是鸡同鸭讲。
OGNL(Object-Graph Navigation Language)是MyBatis用来解析动态SQL表达式的引擎。当它遇到比较操作时,会尝试进行类型转换以便比较。但对于Date和String这种完全不兼容的类型,OGNL会直接拒绝转换。
举个例子,当你写createTime != ''时,OGNL的处理流程是这样的:
这里有个常见的误解:开发者往往认为空字符串""可以表示"没有日期"。但实际上:
就像你不能问"这个苹果等于红色吗"一样,Date和String属于不同维度的概念,比较它们本身就没有意义。
对于大多数场景,其实只需要检查Date是否为null就足够了:
xml复制<if test="createTime != null">
AND create_time = #{createTime}
</if>
这种写法:
如果确实需要处理前端可能传空字符串的情况,可以这样改造:
java复制// 在Java代码中提前转换
if(StringUtils.isEmpty(dateParam)){
dateParam = null;
}
或者在XML中使用类型转换:
xml复制<if test="createTime != null and createTime != '1970-01-01'">
AND create_time = #{createTime}
</if>
对于需要频繁进行日期判断的复杂项目,可以考虑实现自定义的OGNL比较器:
java复制public class DateOgnlComparator {
public static boolean compare(Date date, String pattern) {
if(date == null) return false;
return new SimpleDateFormat("yyyy-MM-dd").format(date).equals(pattern);
}
}
然后在MyBatis配置中注册:
xml复制<typeHandlers>
<typeHandler handler="com.example.DateOgnlComparator"/>
</typeHandlers>
这样就能在XML中安全比较:
xml复制<if test="@com.example.DateOgnlComparator@compare(createTime, '') == false">
<!-- 业务逻辑 -->
</if>
不同数据库对日期类型的处理也有差异:
建议在参数中明确指定jdbcType:
xml复制#{createTime,jdbcType=TIMESTAMP}
即使解决了类型比较问题,还要注意时区转换:
xml复制<if test="createTime != null">
AND create_time = CONVERT_TZ(#{createTime}, 'UTC', 'Asia/Shanghai')
</if>
对于大表查询,避免在条件中使用函数转换:
xml复制<!-- 不推荐 -->
<if test="createTime != null">
AND DATE_FORMAT(create_time, '%Y-%m-%d') = #{createTime}
</if>
<!-- 推荐 -->
<if test="createTime != null">
AND create_time BETWEEN #{createTime} AND #{createTime}+1
</if>
好的代码必须要有测试验证。针对日期比较场景,建议编写如下测试用例:
java复制@Test
public void testDateComparison() {
// 测试null值
Map<String, Object> params = new HashMap<>();
params.put("createTime", null);
List<User> users = userMapper.selectByCreateTime(params);
Assert.assertEquals(0, users.size());
// 测试有效日期
params.put("createTime", new Date());
users = userMapper.selectByCreateTime(params);
Assert.assertTrue(users.size() > 0);
// 测试空字符串(应该提前在Service层转换为null)
params.put("createTime", "");
try {
users = userMapper.selectByCreateTime(params);
Assert.fail("应该抛出异常");
} catch(Exception e) {
Assert.assertTrue(e instanceof IllegalArgumentException);
}
}
最后我们深入MyBatis源码,看看这个异常是怎么抛出的。关键代码在ognl.OgnlOps类中:
java复制public static int compareWithConversion(Object v1, Object v2) {
// 如果两个对象都是null,认为相等
if (v1 == null && v2 == null) {
return 0;
}
// 如果类型不兼容,直接抛出异常
if (v1 != null && v2 != null && !isNumeric(v1) && !isNumeric(v2)) {
throw new IllegalArgumentException("invalid comparison: "
+ v1.getClass().getName() + " and " + v2.getClass().getName());
}
// 其他比较逻辑...
}
这段代码清楚地告诉我们:当比较两个非数字类型且不兼容的对象时,OGNL会直接拒绝比较。理解了这个原理,就能从根本上避免这类问题。