在Java后端开发中,MyBatis的动态SQL是日常高频使用的功能,但很多开发者都会在集合参数(List/数组/Map)的取值上踩坑。比如最近有同学遇到这样的问题:
用
List<LocalDateTime>作为时间范围参数,在XML里写#{submitDate.get(0)}时直接报错,换成#{submitDate[0]}反而正常了。更奇怪的是,之前用LocalDateTime[]数组时还触发了TypeHandler异常。
这背后其实是MyBatis动态SQL的核心语法------OGNL表达式在起作用。本文将从实际报错场景出发,彻底讲清楚OGNL与Java原生语法的区别,以及集合参数在动态SQL中的正确姿势。
一、场景还原:从一个TypeHandler报错说起
先看一段真实的业务代码:
java
// Mapper接口(原错误写法:用数组作为参数)
List<RepairOrder> queryOrders(
@Param("submitDate") LocalDateTime[] submitDate,
@Param("repairDate") LocalDateTime[] repairDate
);
对应的XML映射文件:
xml
<if test="submitDate != null">
AND b.submit_date >= #{submitDate[0]}
AND b.submit_date <= #{submitDate[1]}
</if>
<if test="repairDate != null">
AND b.repair_date >= #{repairDate[0]}
AND b.submit_date <= #{repairDate[1]} <!-- 此处还存在笔误 -->
</if>
运行后直接抛出异常:
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: java.lang.IllegalStateException: Type handler was null on parameter mapping for property 'repairDate[0]'. It was either not specified and/or could not be found for the javaType ([Ljava.time.LocalDateTime;) : jdbcType (null) combination.
问题拆解
- 表层问题 :MyBatis默认不支持
LocalDateTime[]数组类型的TypeHandler,导致参数解析失败。 - 深层问题:开发者混淆了MyBatis的OGNL语法与Java原生语法,同时对集合参数的处理逻辑不清晰。
- 额外笔误 :
repairDate的结束条件错误关联到submit_date字段,导致业务逻辑错误。
二、核心原理:MyBatis的OGNL表达式不是Java语法
MyBatis的动态SQL(<if>/#{}/${}等)底层依赖**OGNL(Object-Graph Navigation Language)**表达式引擎,而非原生Java代码。OGNL为了简化对象与集合的访问,设计了一套更简洁的语法规则,这也是很多开发者踩坑的根源。
关键区别:OGNL vs 原生Java语法
| 操作场景 | 原生Java语法 | MyBatis OGNL语法(推荐) | 底层解析逻辑 |
|---|---|---|---|
| List元素取值 | list.get(0) |
list[0] |
自动调用list.get(0) |
| 数组元素取值 | array[0] |
array[0] |
原生数组下标访问 |
| Map元素取值 | map.get("key") |
map.key 或 map["key"] |
自动调用map.get("key") |
| 对象方法调用 | user.getName() |
user.name 或 user.getName() |
优先调用getter方法,无getter则直接访问字段 |
为什么List不能用get(0)?
OGNL虽然支持调用对象方法(如list.size()),但在#{}参数占位符中,submitDate.get(0)会被MyBatis解析为参数名的一部分,而非方法调用。例如:
xml
<!-- 错误写法:MyBatis会认为参数名是"submitDate.get(0)" -->
AND b.submit_date >= #{submitDate.get(0)}
运行后会抛出参数找不到的异常:
Could not find parameter 'submitDate.get(0)' in parameter map
而#{submitDate[0]}是OGNL专门为集合设计的语法糖,MyBatis会自动识别submitDate是List类型,并调用get(0)方法获取元素,这才是合法的取值方式。
三、实战验证:List/数组在动态SQL中的正确姿势
方案1:优先用List替代数组(推荐)
MyBatis对List的支持远好于数组,无需额外配置TypeHandler,只需修改Mapper接口的参数类型:
java
// 修正后的Mapper接口(数组→List)
List<RepairOrder> queryOrders(
@Param("submitDate") List<LocalDateTime> submitDate,
@Param("repairDate") List<LocalDateTime> repairDate
);
对应的XML映射文件(含笔误修复):
xml
<!-- 正确写法:用OGNL的[]语法访问List元素 -->
<if test="submitDate != null and submitDate.size() > 1">
AND b.submit_date >= #{submitDate[0]}
AND b.submit_date <= #{submitDate[1]}
</if>
<if test="repairDate != null and repairDate.size() > 1">
AND b.repair_date >= #{repairDate[0]}
AND b.repair_date <= #{repairDate[1]} <!-- 修复笔误:submit_date→repair_date -->
</if>
- 关键优化 :新增
submitDate.size() > 1判断,避免List元素不足时出现下标越界。 - 运行效果 :MyBatis自动解析
submitDate[0]为submitDate.get(0),无需额外TypeHandler,参数解析正常。
方案2:必须用数组时的兼容处理
如果业务场景强制要求用数组(如老系统兼容),需自定义TypeHandler来支持LocalDateTime[]类型:
java
@MappedTypes(LocalDateTime[].class)
@MappedJdbcTypes(JdbcType.TIMESTAMP)
public class LocalDateTimeArrayTypeHandler extends BaseTypeHandler<LocalDateTime[]> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, LocalDateTime[] parameter, JdbcType jdbcType) throws SQLException {
if (parameter.length > 0) {
ps.setObject(i, parameter[0]); // 适配数组下标访问
}
}
// 省略其他方法...
}
注册TypeHandler后,在XML中指定类型处理器:
xml
<if test="submitDate != null">
AND b.submit_date >= #{submitDate[0],typeHandler=com.xxx.handler.LocalDateTimeArrayTypeHandler}
</if>
四、避坑指南:OGNL表达式的常见误区
1. 误区:判断List非空只用list != null
正确写法需同时判断size() > 0,避免空List导致下标越界:
xml
<!-- 错误:空List会导致submitDate[0]报错 -->
<if test="submitDate != null">
<!-- 正确:确保List有元素 -->
<if test="submitDate != null and submitDate.size() > 0">
2. 误区:Map取值用map.get("key")
推荐用更简洁的map.key或map["key"]:
xml
<!-- 推荐写法 -->
#{params.startTime}
#{params["startTime"]}
<!-- 不推荐写法 -->
#{params.get("startTime")}
3. 误区:对象属性访问用getter方法
OGNL优先调用getter方法,但直接用属性名更简洁:
xml
<!-- 推荐写法 -->
#{user.name}
<!-- 等价写法(不推荐) -->
#{user.getName()}
五、总结
MyBatis动态SQL的坑,本质上是OGNL语法与Java原生语法的差异导致的。记住这几个核心结论:
- 集合取值用
[]:List/数组统一用list[0],Map用map.key或map["key"]。 - 优先用List:MyBatis对List的支持更完善,避免数组的TypeHandler问题。
- 判断非空要严谨 :List需同时判断
!= null和size() > 0,防止下标越界。 - 避免语法混淆 :OGNL不是Java,不要在
#{}中写get()方法。