在 MyBatis 中,常常会遇到集合类型的参数,虽然我们可以通过 OGNL 表达式来访问集合的某一个元素,但是 OGNL 表达式无法遍历集合。foreach 标签就是专门用来解决这类问题的,foreach 标签可以用来遍历数组、列表和 Map 等集合参数,实现批量操作或一些简单 SQL 操作。
foreach 元素的属性主要有 item,index,open,separator,close,collection。各属性含义如下所示。
集合中元素迭代时的别名,该参数为必选。
在 list 和数组中,index 是元素的序号;在 map 中,index 是元素的 key。该参数可选。
foreach 代码的开始符号,一般是 ”(“,和 close=“)” 合用。常用在 in(),values() 时。该参数可选。
元素之间的分隔符,例如在 in() 的时候,separator=“,” 会自动在元素中间用 “,“ 隔开,避免手动输入逗号导致 SQL 错误,如 in(1, 2,) 这样。该参数可选。
foreach 代码的关闭符号,一般是 ”)“,和 open=“(” 合用。常用在 in(),values()时。该参数可选。
要被 foreach 标签循环解析的对象。
foreach 标签的 collection 属性在接受参数名时,有两种情况:
匿名参数
当在 java 方法中没有通过 @Param 注解指定参数名时,列表类型默认参数名为 ”list“,数组类型默认参数名为 ”array“,Map 对象没有默认值。
具名参数
java 方法中使用了 @Param 注解指定了参数名称,则 foreach 中的 collection 属性必须为参数名。
在作为入参时可以使用 @Param(“keyName”) 来设置该键值,设置 keyName 后,list、array 将会失效。除了入参这种情况外,还有一种是作为参数对象的某个字段,例子如下。如果 User 有属性 List ids。入参是 User 对象,那么这个collection = “ids”。如果 User 有属性 Ids ids,其中 Ids 是个对象,Ids 有个属性 List id,入参是 User 对象,那么 collection = “ids.id”。
注意点:在使用 foreach 的时候,最关键的就是 collection 属性,该属性是必须指定的,但是在不同情况下,该属性的值是不一样的,主要有以下 3 种情况(未通过 @Param 指定别名时)。
在使用参数的过程中,会遇到 #{} 与 ${} 的问题,因此简单总结下两者之间的区别。
${param} 传递的参数会被当成 SQL 语句中的一部分,比如传递表名,字段名,字段类型等数据。
例如,传入值为 id,order by ${param} 则解析成 SQL:order by id。
#{parm} 传入的数据都当成一个字符串,会对自动传入的数据加一个双引号。
例如,传入值为 id,select * from table where name = #{param} 则解析成 SQL:select * from table where name = ? (其中问号在执行时传入值 “id”)。
为了安全,能用 # 的地方就用 # 方式传参,这样可以有效的防止 SQL 注入攻击。
官方说明:mybatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为 “?”,调用 PreparedStatement 的 set 方法来赋值;mybatis 在处理 ${} 时,就是把 ${} 替换成变量的值。使用 #{} 可以有效的防止 SQL 注入,提高系统安全性。
本文以如下几个例子简单总结下 foreach 是如何遍历列表、数组和 Map 的。
Java 层接口
List<Rule> selectRulesByList(List<String> ids);
XML
<select id="selectRulesByList" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List" />
FROM tbl_test_rule WHERE rule_id IN
<foreach collection="list" open="(" close=")" separator="," item="item" index="index">
#{item}
foreach>
select>
运行时 SQL 语句
==> Preparing: SELECT rule_id, rule_name, rule_type FROM tbl_test_rule WHERE rule_id IN ( ? , ? )
==> Parameters: 10001(String), 20002(String)
<== Total: 2
项目中定义了一个实体类 Rule,在批量插入时需要遍历 List
Java 层接口
int insertRules(@Param("rules") List<Rule> rules);
XML
<insert id="insertRules">
INSERT INTO tbl_test_rule (rule_id, rule_name, rule_type) VALUES
<foreach collection="rules" separator="," item="rule">
(#{rule.ruleId}, #{rule.ruleName}, #{rule.ruleType})
foreach>
insert>
运行时 SQL 语句
==> Preparing: INSERT INTO tbl_test_rule (rule_id, rule_name, rule_type) VALUES (?, ?, ?) , (?, ?, ?)
==> Parameters: ruleId1(String), ruleName1(String), 1(String), ruleId2(String), ruleName2(String), 2(String)
<== Updates: 2
Java 层接口
List<Rule> selectRulesByArray(String[] ids);
如果 ids 参数使用 @Param 注解指定了参数名称,则 foreach 标签中的 collection 属性必须为该名称;但若未指定名称,则在 foreach 标签中使用默认数组名称 array,如下所示
XML
<select id="selectRulesByArray" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List" />
FROM tbl_test_rule WHERE rule_id IN
<foreach collection="array" open="(" close=")" separator="," item="item" index="index">
#{item}
foreach>
select>
运行时 SQL 语句
==> Preparing: SELECT rule_id, rule_name, rule_type FROM tbl_test_rule WHERE rule_id IN ( ? , ? )
==> Parameters: 10001(String), 20002(String)
<== Total: 2
数据库表 tbl_test_discount,联合主键(cert_no, rule_id, cycle_id),存储了不同用户不同周期下的折扣金额。请求参数 Map 中存储了某一用户不同规则(key 为不同的 rule_id 值)和各个规则下的折扣值(value 为 dis_sum),如下方 Java 接口定义。需要实现:当数据库中无主键记录时,将记录插入数据库;如数据库中存在主键记录时,更新折扣值,将折扣值累加计算(即实现 insert into … on duplicate key update 操作)。过程示例如下。
Java 层接口
int saveOrUpd(@Param(value = "certNo") String certNo,
@Param(value = "cycleId") String cycleId,
@Param(value = "params") Map map);
使用 foreach 标签遍历 Map 时,collection 属性值为 @param 注解指定的参数名,即 params,且 item 是 Map 的键值,index 是键名。
XML
<update id="saveOrUpd" parameterType="java.util.Map">
<foreach collection="params" index="key" item="value">
insert into tbl_test_discount (cert_no, rule_id, cycle_id, dis_sum) values
(#{certNo}, #{key}, #{cycleId}, #{value}) on duplicate key update dis_sum = dis_sum + #{value};
foreach>
update>
mybatis 设置允许批量更新
mybatis 会根据上述 XML 文件的配置,动态生成多条 SQL。要让 mybatis 成功执行多条语句,须开启允许批量查询设置,即在 jdbc-url 连接信息中添加 &allowMultiQueries=true,如下所示。
spring.datasource.jdbc-url=jdbc:mysql://localhost:3306/testdb?characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true
MySQL 连接数据库时,添加语句 &allowMultiQueries=true
的作用:可以在 SQL 语句后携带分号,实现多语句执行;可以执行批处理,同时发出多个 SQL 语句。
运行时 SQL 语句
==> Preparing: insert into tbl_test_discount (cert_no, rule_id, cycle_id, dis_sum) values (?, ?, ?, ?) on duplicate key update dis_sum = dis_sum + ?; insert into tbl_test_discount (cert_no, rule_id, cycle_id, dis_sum) values (?, ?, ?, ?) on duplicate key update dis_sum = dis_sum + ?; insert into tbl_test_discount (cert_no, rule_id, cycle_id, dis_sum) values (?, ?, ?, ?) on duplicate key update dis_sum = dis_sum + ?; insert into tbl_test_discount (cert_no, rule_id, cycle_id, dis_sum) values (?, ?, ?, ?) on duplicate key update dis_sum = dis_sum + ?;
==> Parameters: testCertNo1(String), 30001(String), 202212(String), 225(Integer), 225(Integer), testCertNo2(String), 20002(String), 202212(String), 385(Integer), 385(Integer), testCertNo3(String), 20001(String), 202212(String), 553(Integer), 553(Integer), testCertNo4(String), 10001(String), 202212(String), 300(Integer), 300(Integer)
<== Updates: 1
foreach 标签是使用非常广泛的一个标签,当使用 SQL 进行批量插入、查询时都可能使用到它。列表遍历的使用最为广泛,数组和 Map 则相对较少。