MyBatis 多参数、对象和集合参数是如何绑定的?
简化版
MyBatis 会把 Mapper 方法参数转换成一个供 OGNL 和 #{} 使用的参数对象;单个 JavaBean 可直接按属性访问,多参数建议用 @Param 显式命名。单个 List、Collection 或数组会提供相应集合键,批量 SQL 应用 <foreach> 遍历,不能依赖不同编译配置下不稳定的真实参数名。
详细版
一个复杂对象参数可写 #{user.name} 或直接按根对象属性写 #{name};多个普通参数会经 ParamNameResolver 组成 Map,@Param("id") 提供稳定名称,同时 MyBatis 还可能提供 param1、param2 等通用名。未标注时能否使用源码参数名取决于 useActualParamName 和是否保留 -parameters 元数据。
单个 Collection 参数可用 collection,List 还可用 list,数组可用 array;若加 @Param("ids"),应使用显式名称 ids。RowBounds、ResultHandler 等特殊参数不会作为普通 SQL 参数参与命名。
完整版教学
一、单个对象参数
User find(UserQuery query);
SELECT * FROM users
WHERE status = #{status}
AND created_at >= #{startTime}
query 本身就是根参数对象,MyBatis 通过 MetaObject/属性访问找到 getter 或字段。嵌套对象可以写 #{address.city},但动态条件要先判断中间对象是否为 null。
| Mapper 形态 | XML 中推荐名字 | 稳定性 | 注意点 |
|---|---|---|---|
| 单个 JavaBean | #{name}、#{query.name} 视包装而定 | 较稳定 | 嵌套属性要防 null |
| 多个普通参数 | @Param 指定的名字 | 最稳定 | 不依赖编译参数名 |
| 单个 List | list 或显式 @Param("ids") | 显式注解更稳定 | foreach 展开 |
| 单个数组 | array 或显式名称 | 显式注解更稳定 | foreach 展开 |
| Map 参数 | Map key | 灵活但弱类型 | 键拼错运行期才发现 |
记忆钩子:Mapper 参数进入 MyBatis 后都会变成“一个可被 OGNL 读取的参数对象”;多参数时,
@Param就是在给这个对象的 key 固定名字。
二、多个参数为什么建议 @Param
User find(@Param("id") long id,
@Param("status") String status);
XML 可稳定使用 #{id} 和 #{status}。不加注解时,MyBatis 可能使用实际参数名,也会提供通用名称,但实际参数名是否存在受编译器 -parameters 影响;重构参数名也可能悄悄改变 XML 契约。
@Param 把 Mapper API 与 SQL 映射的名字显式固定,更适合公共方法和多人维护项目。
方法: find(long id, String status)
不加 @Param 时可能可用:
#{arg0}, #{arg1}
#{param1}, #{param2}
若保留 -parameters,也可能有 #{id}, #{status}
加 @Param 后稳定可用:
#{id}, #{status}
同时仍可能有 param1/param2
假设某团队本地编译保留了 -parameters,线上构建没保留,XML 里写 #{status} 就可能本地可用、线上失败。@Param 的价值就在于把这个契约从“编译器是否保留元数据”变成“源码显式声明”。
三、集合参数如何展开
List<User> findByIds(@Param("ids") List<Long> ids);
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
foreach 为每个元素生成独立参数映射。空集合可能生成非法 IN () 或没有条件,调用前或动态 SQL 中必须明确“空列表返回空结果还是忽略条件”,不能任由它改变查询范围。
四、Map 参数的取舍
Map 可以灵活传任意键,但缺少编译期属性检查,键拼错只能运行时报错。固定业务查询优先定义 Query DTO;只有真正动态、字段集合不固定的框架层场景才使用 Map。
返回 Map 与参数 Map 也不要混淆,前者常由 @MapKey 指定结果键,后者只是输入容器。
五、OGNL 与动态条件
<if test="name != null and name != ''"> 使用 OGNL 读取参数。OGNL 表达式负责是否拼接 SQL,#{} 负责 JDBC 值绑定,两者处在不同阶段。
条件过于复杂时应在 Java 中先计算明确字段,避免把大量业务规则塞进 XML 表达式。对字符串、数字和枚举的比较也要注意类型,不要用偶然的隐式转换。
六、特殊参数和空集合边界
RowBounds、ResultHandler 这类特殊参数通常不参与普通 SQL 参数命名,它们影响 MyBatis 执行行为,而不是作为 #{} 值绑定。面试中如果被问“为什么我第 2 个参数不是 param2”,要想到特殊参数会被跳过。
集合参数还要处理空集合。比如 ids = [] 时,WHERE id IN () 在很多数据库是非法 SQL;如果你用动态 SQL 把整个 IN 条件去掉,可能从“查不到任何用户”变成“查全部用户”。因此空集合最好在 Service 层直接返回空列表,或者在 SQL 中显式生成不成立条件,如 1 = 0。
七、常见误区与追问
- 误区:多参数可以长期依赖真实参数名。 真实参数名依赖
useActualParamName和编译元数据,公共 Mapper 方法应使用@Param固定名称。 - 误区:
param1、param2比@Param更推荐。 通用名可用但可读性差,参数顺序变更时也容易误绑定。 - 追问:单个 List 为什么常见
list这个名字? MyBatis 会为单个 List/Collection/数组提供默认集合键,但显式@Param("ids")更清晰。 - 追问:foreach 空集合怎么处理? 必须定义业务语义,通常在 Service 直接返回空结果,或生成不成立条件,不能让条件静默消失。
- 误区:Map 参数最灵活所以最适合业务查询。 Map 没有编译期字段检查,固定查询更推荐 Query DTO。
- 追问:OGNL 和
#{}分别负责什么? OGNL 在动态 SQL 阶段判断和取值,#{}在 JDBC 阶段生成占位符并绑定参数。
八、加强记忆
单对象按属性取,多参数用 @Param 固定名字,集合用 foreach 展开。实际参数名受编译元数据影响,param1 等通用名可用但可读性差;参数契约越明确,XML 与 Mapper 重构越安全。