MyBatis动态SQL避坑:为什么List用`[0]`而不是`get(0)`

在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.

问题拆解

  1. 表层问题 :MyBatis默认不支持LocalDateTime[]数组类型的TypeHandler,导致参数解析失败。
  2. 深层问题:开发者混淆了MyBatis的OGNL语法与Java原生语法,同时对集合参数的处理逻辑不清晰。
  3. 额外笔误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.keymap["key"] 自动调用map.get("key")
对象方法调用 user.getName() user.nameuser.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.keymap["key"]

xml 复制代码
<!-- 推荐写法 -->
#{params.startTime}
#{params["startTime"]}

<!-- 不推荐写法 -->
#{params.get("startTime")}

3. 误区:对象属性访问用getter方法

OGNL优先调用getter方法,但直接用属性名更简洁:

xml 复制代码
<!-- 推荐写法 -->
#{user.name}

<!-- 等价写法(不推荐) -->
#{user.getName()}

五、总结

MyBatis动态SQL的坑,本质上是OGNL语法与Java原生语法的差异导致的。记住这几个核心结论:

  1. 集合取值用[] :List/数组统一用list[0],Map用map.keymap["key"]
  2. 优先用List:MyBatis对List的支持更完善,避免数组的TypeHandler问题。
  3. 判断非空要严谨 :List需同时判断!= nullsize() > 0,防止下标越界。
  4. 避免语法混淆 :OGNL不是Java,不要在#{}中写get()方法。
相关推荐
不凡而大米、2 小时前
报错:传入的请求具有过多的参数。该服务器支持最多2100个参数
java·开发语言·mybatis
BD_Marathon2 小时前
MyBatis的一级缓存
spring·缓存·mybatis
啊吧怪不啊吧3 小时前
极致性能的服务器Redis之Hash类型及相关指令介绍
大数据·数据库·redis·sql·mybatis·哈希算法
Mr1ght3 小时前
高并发场景下 JSQLParser 性能瓶颈及替代方案实践
java·数据库·sql
engchina17 小时前
自然语言转 SQL 并不是“魔法”
数据库·人工智能·sql·text2sql·nl2sql·自然语言转sql
小马爱打代码19 小时前
MyBatis:反射模块详解
mybatis
BD_Marathon19 小时前
动态SQL(六)foreach标签2
数据库·sql
IT大白19 小时前
1、一条SQL是如何执行的
数据库·sql
独自破碎E20 小时前
MySQL中如何进行SQL调优?
数据库·sql·mysql