3 动态sql
约 9077 字大约 30 分钟
2025-12-19
学习目标
- 掌握MyBatis中动态SQL元素的使用
- 掌握MyBatis的条件查询操作
- 掌握MyBatis的更新操作
- 掌握MyBatis的复杂查询操作
在实际项目的开发中,开发人员在使用JDBC或其他持久层框架进行开发时,经常需要根据不同的条件拼接SQL语句,拼接SQL语句时还要确保不能遗漏必要的空格、标点符号等,这种编程方式给开发人员带来了极大的不便。而MyBatis 提供的 SQL语句动态组装功能,能很好地解决这一问题。
3.1 动态SQL中的元素
动态SQL是MyBatis的强大特性之一,MyBatis采用了功能强大的基于对象导航图语言(Object Graph Navigation Language,OGNL)的表达式来完成动态SQL。在MyBatis的映射文件中,开发人员可通过动态SQL元素灵活组装SQL语句,这在很大程度上避免了单一SQL语句的反复堆砌,提高了SQL语句的复用性。
MyBatis动态SQL中的常用元素如表3-1所示。
表3-1 MyBatis动态SQL中的常用元素
| 元素 | 说明 |
|---|---|
<if> | 判断语句,用于单条件判断 |
<choose> (<when>、<otherwise>) | 相当于Java中的switch...case...default语句,用于多条件判断 |
<where> | 简化SQL语句中where的条件判断 |
<trim> | 可以灵活地去除多余的关键字 |
<set> | 用于SQL语句的动态更新 |
<foreach> | 循环语句,常用于in语句等列举条件中 |
3.2 条件查询操作
3.2.1 <if>元素
在MyBatis中,<if>元素是最常用的判断元素,它类似于Java中的if语句,主要用于实现某些简单的条件判断。
在实际应用中,常会通过某个条件查询某个数据。例如,要查找某个客户的信息,可以通过姓名或者年龄来查找客户,也可以不填写年龄直接通过姓名来查找客户,还可以什么都不填写查询出所有客户,此时姓名和年龄就是非必须条件。遇到上述情况,可在MyBatis中通过<if>元素来实现。下面通过一个具体的案例演示单条件判断下<if>元素的使用:
- 数据库准备
在名称为heima_ssm_book 的数据库中,创建一个ch3_t_customer数据表,并插入几条测试数据,具体代码如下:
use heima_ssm_book;
create table ch3_t_customer
(
id int(32) primary key auto_increment comment '用户id',
username varchar(50) comment '用户名',
jobs varchar(50) comment '职业',
phone varchar(16) comment '电话'
);
insert into ch3_t_customer (id, username, jobs, phone)
values (1, 'joy', 'student', '13512345678');
insert into ch3_t_customer (id, username, jobs, phone)
values (2, 'jack', 'teacher', '13512345679');
insert into ch3_t_customer (id, username, jobs, phone)
values (3, 'tom', 'engineer', '13512345680');完成上述操作后,数据如图所示。

- POJO类准备
在src/main/java/io/weew12/github/pojo/Customer.java创建持久化类Customer,在类中声明id、username、jobs 和 phone 属性,以及属性对应的getter/setter方法。
package io.weew12.github.pojo;
public class Customer {
// 客户id
private Integer id;
// 客户姓名
private String username;
// 职业
private String jobs;
// 电话
private String phone;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getJobs() {
return jobs;
}
public void setJobs(String jobs) {
this.jobs = jobs;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
@Override
public String toString() {
return "Customer{" +
"id=" + id +
", username='" + username + '\'' +
", jobs='" + jobs + '\'' +
", phone='" + phone + '\'' +
'}';
}
}在文件中,持久化类Customer与普通的JavaBean并没有什么区别,只是其属性字段与数据库中的表字段相对应。实际上,Customer就是一个POJO(普通Java对象),MyBatis就是采用POJO 作为持久化类来完成对数据库的操作。
- 创建接口文件
在src/main/java/io/weew12/github/mapper/CustomerMapper.java创建查询接口文件CustomerMapper.java
package io.weew12.github.mapper;
import io.weew12.github.pojo.Customer;
public interface CustomerMapper {
Customer findCustomerByNameAndJobs(Customer customer);
}- 创建映射文件
在项目下创建映射文件io/weew12/github/mapper/CustomerMapper.xml,在映射文件中,根据客户姓名和职业组合成的条件查询客户信息,使用<if>元素编写该组合条件的动态SQL。CustomerMapper.xml具体代码如文件3-2所示。
文件3-2CustomerMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.weew12.github.mapper.CustomerMapper">
<!-- if 元素的使用-->
<select id="findCustomerByNameAndJobs" parameterType="io.weew12.github.pojo.Customer"
resultType="io.weew12.github.pojo.Customer">
select *
from heima_ssm_book.ch3_t_customer
where 1 = 1
<if test="username != null and username != ''">
and username like concat('%', #{username}, '%')
</if>
<if test="jobs != null and jobs != ''">
and jobs = #{jobs}
</if>
</select>
</mapper>在文件中,使用<if>元素的test属性分别对username和jobs进行了非空判断,如果传入的查询条件非空就进行动态SQL组装。
<if>元素的test属性多用于条件判断语句中,用于判断真假,在大部分的场景中都用于进行非空判断,有时候也用于判断字符串、数字和枚举等。
- 修改核心配置文件
在配置文件mybatis-config.xml中,引入CustomerMapper.xml映射文件,将CustomerMapper.xml映射文件加载到程序中:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="db.properties"/>
<typeAliases>
</typeAliases>
<!-- 数据库连接环境设置-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${mysql.driver}"/>
<property name="url" value="${mysql.url}"/>
<property name="username" value="${mysql.username}"/>
<property name="password" value="${mysql.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="io/weew12/github/mapper/CustomerMapper.xml"/>
</mappers>
</configuration>- 创建获取SqlSession对象的工具类
在项目的src/main/java目录下创建MyBatisUtils类:
package io.weew12.github.utils;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.Reader;
public class MybatisUtils {
private static SqlSessionFactory sqlSessionFactory;
static {
try {
// 使用 MyBatis 提供的 Resources 类加载 MyBatis 的配置文件
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
// 构建 SqlSessionFactory 对象
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取 SqlSession 对象的静态方法
*
* @return SqlSessionFactory
*/
public static SqlSession getSession() {
return sqlSessionFactory.openSession();
}
}- 测试类
在测试类MyBatisTest中,编写测试方法findCustomerByNameAndJobsTest,该方法用于根据客户姓名和职业组合条件查询客户信息列表。MyBatisTest类具体代码如文件3-3所示。
package io.weew12.github;
import io.weew12.github.mapper.CustomerMapper;
import io.weew12.github.pojo.Customer;
import io.weew12.github.utils.MybatisUtils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
import java.util.List;
public class MyBatisTest {
/**
* 根据客户姓名和职业组合条件查询客户信息列表
*/
@Test
public void findCustomerByNameAndJobsTest() {
// 通过工具类获取 SqlSession 对象
SqlSession sqlSession = MybatisUtils.getSession();
// 创建Customer 对象,封装需要组合查询的条件
Customer customer = new Customer();
customer.setUsername("jack");
customer.setJobs("teacher");
// 执行 Mapper代理 的查询方法,返回结果集
CustomerMapper customerMapper = sqlSession.getMapper(CustomerMapper.class);
List<Customer> customerByNameAndJobs = customerMapper.findCustomerByNameAndJobs(customer);
// 输出查询结果信息
for (Customer customerByNameAndJob : customerByNameAndJobs) {
System.out.println(customerByNameAndJob);
}
// 关闭 sqlSession
sqlSession.close();
}
}在文件中,通过MyBatisUtils工具类获取SqlSession对象;使用Customer对象封装用户名为jack且职业为teacher的查询条件,并通过CustomerMapper代理对象调用findCustomerByNameAndJobs方法执行多条件组合的查询操作;使用for循环打印查询结果信息;最后关闭SqlSession,释放资源。
执行MyBatisTest测试类的findCustomerByNameAndJobsTest()方法,控制台的输出结果如图3-2所示。
从图可以看出,程序查询出了username为jack,并且jobs为teacher的客户信息。

如果将 mapperxml 文件中的<if>元素相关代码注释掉,不使用<if>元素对客户信息进行判断(即不以客户姓名和职业为判断条件),然后再次执行测试类的findCustomerByNameAndJobsTest()方法。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.weew12.github.mapper.CustomerMapper">
<!-- if 元素的使用-->
<select id="findCustomerByNameAndJobs" parameterType="io.weew12.github.pojo.Customer"
resultType="io.weew12.github.pojo.Customer">
select *
from heima_ssm_book.ch3_t_customer
where 1 = 1
<!-- <if test="username != null and username != ''">-->
<!-- and username like concat('%', #{username}, '%')-->
<!-- </if>-->
<!-- <if test="jobs != null and jobs != ''">-->
<!-- and jobs = #{jobs}-->
<!-- </if>-->
</select>
</mapper>从图可以看到,程序查询出了数据表中的所有数据。

3.2.2 <choose>、<when>、<otherwise>元素
在使用<if>元素时,只要test属性中的表达式为true,就会执行元素中的条件语句,但是在实际应用中,有时只需要从多个选项中选择一个去执行。
例如,下面的场景。
- 当客户名称不为空,则只根据客户名称进行客户筛选。
- 当客户名称为空,而客户职业不为空,则只根据客户职业进行客户筛选。
- 当客户名称和客户职业都为空,则要求查询出所有电话不为空的客户信息。
此种情况下,使用<if>元素进行处理是非常不合适的。针对上述情况,MyBatis提供了<choose>、<when>、<otherwise>元素进行处理,这3个元素往往组合在一起使用,作用相当于Java中的switch..case...default语句。
下面演示如何使用<choose>、<when>、<otherwise>元素组合去实现上面所述场景,具体如下:
(1)在映射文件CustomerMapper.xml中,添加使用<choose>、<when>、<otherwise>元素执行上述情况的动态SQL,具体代码如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.weew12.github.mapper.CustomerMapper">
<!-- <choose>(<when>、<otherwise>)元素使用 -->
<select id="findCustomerByNameOrJobsOrPhone" parameterType="io.weew12.github.pojo.Customer"
resultType="io.weew12.github.pojo.Customer">
select *
from heima_ssm_book.ch3_t_customer
where 1 = 1
<choose>
<when test="username != null and username != ''">
and username like concat('%', #{usename}, '%')
</when>
<when test="jobs != null and jobs != ''">
and jobs = #{jobs}
</when>
<otherwise>
and phone is not null
</otherwise>
</choose>
</select>
</mapper>上述配置代码中,使用<choose>元素进行SQL拼接,当第一个<when>元素中的条件为真时,只动态组装第一个<when>元素内的SQL片段并执行,否则就继续向下判断第二个<when>元素中的条件是否为真,以此类推,直到某个<when>元素中的条件为真,结束判断。当前面所有<when>元素中的条件都不为真时,则动态组装<otherwise>元素内的SQL片段并执行。
(2)在测试类MyBatisTest中,编写测试方法findCustomerByNameOrJobsOrPhoneTest,该方法用于根据客户姓名或职业查询客户信息列表,具体代码如下:
@Test
public void findCustomerByNameOrJobsTest() {
SqlSession session = MyBatisUtils.getSession();
Customer customer = new Customer();
customer.setUsername("tom");
customer.setJobs("teacher");
List<Customer> customers = session.selectList("com.itheima.mapper.CustomerMapper.findCustomerByNameOrJobs", customer);
for (Customer customer2 : customers) {
// 打印输出结果
System.out.println(customer2);
}
session.close();
}执行测试类MyBatisTest的findCustomerByNameOrJobsOrPhoneTest方法,控制台的输出结果如图所示。

由图所示的输出结果进行分析可知,虽然同时传入了姓名和职业两个查询条件,但MyBatis只是动态组装了客户姓名的SQL片段进行条件查询。
如果将上述代码中的代码customer.setUsername("tom");删除或者注释掉,使SQL只按职业进行查询,再次执行findCustomerByNameOrJobsOrPhoneTest方法后,控制台的输出结果如图所示。
/**
* - 根据客户姓名或职业查询客户信息列表
*/
@Test
public void findCustomerByNameOrJobsOrPhoneTest() {
// 通过工具类获取 SqlSession 对象
SqlSession sqlSession = MybatisUtils.getSession();
// 创建Customer 对象,封装需要组合查询的条件
Customer customer = new Customer();
// customer.setUsername("tom");
customer.setJobs("teacher");
// 执行Mapper代理的查询方法,返回结果集
CustomerMapper customerMapper = sqlSession.getMapper(CustomerMapper.class);
List<Customer> customerByNameOrJobsOrPhone = customerMapper.findCustomerByNameOrJobsOrPhone(customer);
// 输出查询结果信息
for (Customer customer1 : customerByNameOrJobsOrPhone) {
System.out.println(customer1);
}
// 关闭 sqlSession
sqlSession.close();
}
对图中的输出结果进行分析可知,MyBatis生成了按客户职业进行查询的SQL语句,同样查询出了客户信息。
如果将上述代码中的代码customer.setUsername("tom");和customer.setJobs("teacher");都删除或者注释掉(即客户姓名和职业都为空),那么程序的执行结果如图所示。
/**
* - 根据客户姓名或职业查询客户信息列表
*/
@Test
public void findCustomerByNameOrJobsOrPhoneTest() {
// 通过工具类获取 SqlSession 对象
SqlSession sqlSession = MybatisUtils.getSession();
// 创建Customer 对象,封装需要组合查询的条件
Customer customer = new Customer();
// customer.setUsername("tom");
// customer.setJobs("teacher");
// 执行Mapper代理的查询方法,返回结果集
CustomerMapper customerMapper = sqlSession.getMapper(CustomerMapper.class);
List<Customer> customerByNameOrJobsOrPhone = customerMapper.findCustomerByNameOrJobsOrPhone(customer);
// 输出查询结果信息
for (Customer customer1 : customerByNameOrJobsOrPhone) {
System.out.println(customer1);
}
// 关闭 sqlSession
sqlSession.close();
}
对图中的输出结果进行分析可知,当姓名和职业参数都为空时,MyBatis组装了<otherwise>元素中的SQL片段进行条件查询。
3.2.3 <where>、<trim>元素
在3.2.1节和3.2.2节的案例中,映射文件中编写的SQL后面都加入了where 1=1的条件,既保证了where后面的条件成立,又避免了where后面第一个词是and或者or之类的关键字。如果将where后1=1的条件去掉,MyBatis所拼接出来的SQL语句如下:
select * from ch3_t_customer where and username like concat('%', ?, '%') and jobs = #{jobs}上述SQL语句中,where后直接跟的是and,在运行时会报SQL语法错误,针对这种情况,可以使用MyBatis提供的<where>元素和<trim>元素进行处理。
<where>元素
以3.2.1节的案例为例,将文件中的where 1=1条件删除,并使用<where>元素替换where 1=1条件:
<!-- where元素的使用 -->
<select id="findCustomerByNameAndJobs" parameterType="io.weew12.github.pojo.Customer"
resultType="io.weew12.github.pojo.Customer">
select *
from heima_ssm_book.ch3_t_customer
<where>
<if test="username != null and username != ''">
and username like concat('%', #{username}, '%')
</if>
<if test="jobs != null and jobs != ''">
and jobs = #{jobs}
</if>
</where>
</select>在上述代码中,**<where>**元素会自动判断由组合条件拼装的SQL语句,只有**<where>**元素内的某一个或多个条件成立时,才会在拼接SQL中加入关键字where,否则将不会添加;即使where之后的内容有多余的“AND”或“OR”,**<where>**元素也会自动将它们去除。
再次执行测试类的findCustomerByNameAndJobsTest()方法,控制台的输出结果如图所示。

<trim>元素
除了使用<where>元素外,还可以通过<trim>元素来解决上述问题。**<trim>**元素用于删除多余的关键字,它可以直接实现**<where>**元素的功能。<trim>元素包含4个属性,具体如表3-2所示。
表3-2**<trim>**元素的属性
| 属性 | 说明 |
|---|---|
prefix | 指定给SQL语句增加的前缀 |
prefixOverrides | 指定SQL语句中要去掉的前缀字符串 |
suffix | 指定给SQL语句增加的后缀 |
suffixOverrides | 指定SQL语句中要去掉的后缀字符串 |
使用<trim>元素替换文件中的where 1=1条件:
<!-- trim元素的使用 -->
<select id="findCustomerByNameAndJobs" parameterType="io.weew12.github.pojo.Customer"
resultType="io.weew12.github.pojo.Customer">
select *
from heima_ssm_book.ch3_t_customer
<trim prefix="where" prefixOverrides="and">
<if test="username != null and username != ''">
and username like concat('%', #{username}, '%')
</if>
<if test="jobs != null and jobs != ''">
and jobs = #{jobs}
</if>
</trim>
</select>在上述配置代码中,使用<trim>元素对where 1=1条件进行了替换,<trim>元素的作用是去除一些多余的前缀字符串,它的prefix属性是指语句的前缀(where),而prefixOverrides属性是指需要去除的前缀字符串(SQL中的AND或OR)。

3.3 更新操作
在Hibernate框架中,如果想要更新某一个对象,就需要发送所有字段给持久化对象。然而在实际应用中,大多数情况下只需要更新某一个或几个字段。如果每更新一条数据都要将其所有属性都更新一遍,执行效率将非常低。为了解决更新数据的效率问题,MyBatis提供了**<set>**元素。
<set>元素主要用于更新操作,它可以在动态SQL语句前输出关键字SET,并将SQL语句中最后一个多余的逗号去除。使用<set>元素与<if>元素相结合的方式可以只更新需要更新的字段。
下面通过一个案例演示如何使用<set>元素更新数据库的信息:
(0)增加更新接口
package io.weew12.github.mapper;
import io.weew12.github.pojo.Customer;
import java.util.List;
public interface CustomerMapper {
int updateCustomerBySet(Customer customer);
}(1)在映射文件CustomerMapper.xml中添加使用<set>元素执行更新操作的动态SQL,具体代码如下:
<!-- set元素的使用 -->
<update id="updateCustomerBySet" parameterType="io.weew12.github.pojo.Customer">
update heima_ssm_book.ch3_t_customer
<set>
<if test="username != null and username != ''">
username = #{username},
</if>
<if test="jobs != null and jobs != ''">
jobs = #{jobs},
</if>
<if test="phone != null and phone != ''">
phone = #{phone},
</if>
</set>
where id = #{id}
</update>在上述配置的 SQL 语句中,使用<set>元素和<if>元素相结合的方式组装 update 语句。其中,<set>元素会动态前置关键字 SET,同时也会消除 SQL语句中最后一个多余的逗号;<if>元素会判断相应的字段是否有传入值,如果传入的更新字段不是null或空字符串,就将此字段进行动态SQL组装,并更新此字段,否则此字段不执行更新。
(2)为了验证上述配置,可以在测试类中编写测试方法updateCustomerBySetTest(),其代码如下:
/**
* - 更新客户信息
*/
@Test
public void updateCustomerBySetTest() {
// 获取SqlSession
SqlSession sqlSession = MybatisUtils.getSession();
// 创建Customer对象,并向对象中添加数据
Customer customer = new Customer();
customer.setId(3);
customer.setPhone("13311111234");
// 执行Mapper代理的更新方法,返回的是SQL语句影响的行数
CustomerMapper customerMapper = sqlSession.getMapper(CustomerMapper.class);
int i = customerMapper.updateCustomerBySet(customer);
// 通过返回结果判断更新操作是否执行成功
if (i > 0) {
System.out.println("成功更新了" + i + "条数据");
} else {
System.out.println("更新失败");
}
// 提交事务 !!
sqlSession.commit();
// 关闭SqlSession
sqlSession.close();
}在上述代码中,通过MyBatisUtils工具类获取SqlSession对象;创建Customer对象,封装需要更新的字段;执行更新方法,返回的是SQL语句影响的行数;通过返回结果判断更新操作是否执行成功;代码提交事务和关闭SqlSession,释放资源。

执行MyBatisTest测试类的updateCustomerBySetTest方法,控制台的输出结果如图所示。

从图可以看出,程序已经提示成功更新了1条数据。为了验证是否真的执行成功,此时查看t_customer表中的数据,t_customer表更新后的数据如图所示。

由图3-9中的查询结果分析可知,使用<set>元素已成功对数据表中id为3的客户电话进行了修改。
注意:
在映射文件中使用<set>元素和<if>元素组合进行update语句动态SQL组装时,如果**<set>****元素内包含的内容都为空,则会出现SQL语法错误。**因此,在使用**<set>**元素进行字段信息更新时,要确保传入的更新字段不能都为空。
除了使用<set>元素外,还可以通过**<trim>**元素来实现更新操作:
package io.weew12.github.mapper;
import io.weew12.github.pojo.Customer;
import java.util.List;
public interface CustomerMapper {
int updateCustomerByTrim(Customer customer);
} <!-- 基于trim元素更新数据 -->
<update id="updateCustomerByTrim" parameterType="io.weew12.github.pojo.Customer">
update heima_ssm_book.ch3_t_customer
<trim prefix="set" suffixOverrides=",">
<if test="username != null and username != ''">
username = #{username},
</if>
<if test="jobs != null and jobs != ''">
jobs = #{jobs},
</if>
<if test="phone != null and phone != ''">
phone = #{phone},
</if>
</trim>
where id = #{id}
</update>在上述代码中,<trim>元素的prefix属性用于指定要添加的<trim>元素所包含内容的前缀为SET,suffixOverrides属性用于指定去除的<trim>元素所包含内容的后缀为逗号。为了验证上述配置,在MyBatisTest测试类中的测试方法updateCustomerByTrimTest中,将id为3的客户的phone属性的值修改为13311111111:
/**
* - 更新客户信息
*/
@Test
public void updateCustomerByTrimTest() {
// 获取SqlSession
SqlSession sqlSession = MybatisUtils.getSession();
// 创建Customer对象,并向对象中添加数据
Customer customer = new Customer();
customer.setId(3);
customer.setPhone("13311111111");
// 执行Mapper代理的更新方法,返回的是SQL语句影响的行数
CustomerMapper customerMapper = sqlSession.getMapper(CustomerMapper.class);
int i = customerMapper.updateCustomerByTrim(customer);
// 通过返回结果判断更新操作是否执行成功
if (i > 0) {
System.out.println("成功更新了" + i + "条数据");
} else {
System.out.println("更新失败");
}
// 提交事务 !!
sqlSession.commit();
// 关闭SqlSession
sqlSession.close();
}执行 MyBatisTest 测试类的updateCustomerByTrimTest方法,控制台的输出结果如图所示。

从图可以看出,程序已经提示成功更新了1条数据。为了验证更新是否真的执行成功,查看数据库t_customer表中的数据。

从图可以看出,数据表中id为3的客户电话成功被修改了。
3.4 复杂查询操作
在实际开发中,有时可能会遇到这种情况:假设在一个客户表中有1000条数据,现在需要将id值小于100的客户信息全部查询出来,这种情况下,如果每条记录都逐一查询,显然是不可取的。有的人会想到,可以在Java方法中使用循环语句,将查询方法放在循环语句中,通过条件循环的方式查询出所需的数据。这种查询方式虽然可行,但每执行一次循环语句,都需要向数据库中发送一条查询SQL,其查询效率是非常低的。为了解决上述问题,MyBatis提供了用于数组和集合循环遍历的**<foreach>**元素。
3.4.1 <foreach>元素的属性
<foreach>元素主要用于遍历,能够支持数组、**List**或**Set**接口的集合。在实际开发中,<foreach>元素通常和SQL语句中的关键字in结合使用。<foreach>元素的属性如表3-3所示。
表3-3**<foreach>**元素的属性
| 属性 | 说明 |
|---|---|
collection | 用于指定遍历参数的类型。该属性必须指定。主要有以下3种情况: • 若传入参数为单参数且参数类型是一个**List**,**collection**属性值为**list** • 若传入参数为单参数且参数类型是一个数组,**collection**属性值为**array** • 若传入参数为多参数,就需要把参数封装为一个**Map**进行处理,**collection**属性值为**Map**中的**key** |
item | 表示集合中每一个元素进行迭代时的别名。该属性为必选属性 |
index | 在List和数组中,index是元素的序号;在Map中,index是元素的key。该属性为可选属性 |
open | 表示foreach语句代码的开始符号,一般和close=")"合用。常用在in条件语句中。该属性为可选属性 |
separator | 表示元素之间的分隔符,例如,在条件语句中,separator=","会自动在元素中间用,隔开,避免手动输入逗号导致SQL错误 |
close | 表示foreach语句代码的关闭符号,一般和open="("合用。常用在in条件语句中。该属性为可选属性 |
表3-3中列出了<foreach>元素的属性,其中open、close、separator属性用于对遍历内容进行SQL拼接。
3.4.2 <foreach>元素迭代数组
<foreach>元素可以实现数组类型的输入参数的遍历。例如,要从数据表 ch3_t_customer中查询出id为1、2、3的客户信息,如果采用单条查询的方式,势必会造成资源的浪费,此时就可以利用数组作为参数,存储id的属性值1、2、3,并通过<foreach>元素迭代数组完成客户信息的批量查询操作。<foreach>元素迭代数组的实现具体如下。
(0)创建查询接口
package io.weew12.github.mapper;
import io.weew12.github.pojo.Customer;
import java.util.List;
public interface CustomerMapper {
List<Customer> findByArray(Integer[] ids);
}(1)在映射文件CustomerMapper.xml中,添加使用<foreach>元素迭代数组执行批量查询操作的动态SQL:
<!-- foreach元素的使用 -->
<select id="findByArray" parameterType="integer[]" resultType="io.weew12.github.pojo.Customer">
select * from heima_ssm_book.ch3_t_customer
where id in
<foreach collection="array" open="(" close=")" separator="," item="id">
#{id}
</foreach>
</select>在上述配置代码中,使用<foreach>元素迭代数组实现客户信息的批量查询操作。其中,collection属性用于指定传入的参数类型为数组,<foreach>元素将客户id信息存储在数组中,并对数组进行遍历,遍历出的值用于构建SQL语句中的IN条件。
(2)为了验证上述配置,可以在测试类MyBatisTest中,编写测试方法findByArrayTest,该方法代码具体如下:
/**
* - 根据客户id批量查询客户信息
*/
@Test
public void findByArrayTest() {
// 获取SqlSession
SqlSession sqlSession = MybatisUtils.getSession();
// 创建数组,封装查询id
Integer[] ids = {2, 3};
// 执行Mapper代理的查询方法,返回结果集
CustomerMapper customerMapper = sqlSession.getMapper(CustomerMapper.class);
List<Customer> byArray = customerMapper.findByArray(ids);
// 输出查询结果信息
for (Customer customer : byArray) {
System.out.println(customer);
}
// 关闭SqlSession
sqlSession.close();
}在上述代码中,通过MyBatisUtils工具类获取SqlSession对象;创建数组,并封装查询id;执行查询方法,并返回结果集;遍历查询到的结果集信息并输出;关闭SqlSession,释放资源。
执行MyBatisTest 测试类的findByArrayTest方法,控制台的输出结果如图所示。

3.4.3 <foreach>元素迭代List
在3.4.2节中,使用<foreach>元素完成了客户信息的批量查询操作,方法参数为一个数组,现在更改参数类型,传入一个List类型的参数来实现同样的需求。
<foreach>元素迭代List的实现步骤具体如下。
(0)创建查询接口
package io.weew12.github.mapper;
import io.weew12.github.pojo.Customer;
import java.util.List;
public interface CustomerMapper {
List<Customer> findByList(List<Integer> ids);
}(1)在映射文件CustomerMapper.xml中,添加使用<foreach>元素迭代List集合执行批量查询操作的动态SQL:
<select id="findByList" parameterType="list" resultType="io.weew12.github.pojo.Customer">
select * from heima_ssm_book.ch3_t_customer
where id in
<foreach collection="list" open="(" close=")" separator="," item="id">
#{id}
</foreach>
</select>在上述配置代码中,使用<foreach>元素迭代List集合实现了客户信息的批量查询操作。其中,collection属性用于指定传入的参数类型为list。<foreach>元素将客户id信息存储在List集合中,并对List集合进行遍历,遍历出的值用于构建SQL语句中的IN条件。
(2)为了验证上述配置,可以在测试类MyBatisTest中,编写测试方法findByListTest:
/**
* - 根据客户id批量查询客户信息
*/
@Test
public void findByListTest() {
// 获取SqlSession
SqlSession sqlSession = MybatisUtils.getSession();
// 创建List集合,封装查询id
ArrayList<Integer> ids = new ArrayList<Integer>();
ids.add(2);
ids.add(3);
// 执行Mapper代理的查询方法,返回结果集
CustomerMapper customerMapper = sqlSession.getMapper(CustomerMapper.class);
List<Customer> byList = customerMapper.findByList(ids);
// 输出查询结果信息
for (Customer customer : byList) {
System.out.println(customer);
}
// 关闭SqlSession
sqlSession.close();
}执行MyBatisTest 测试类的findByListTest方法,控制台的输出结果如图所示。

3.4.4 <foreach>元素迭代Map
在3.4.2节和3.4.3节中,使用<foreach>元素完成了客户信息的批量查询操作,MyBatis传入参数均为一个参数,如果传入参数为多个参数,就需要把这些参数封装成一个**Map**集合进行处理。
下面通过一个案例演示如何使用<foreach>元素迭代 Map 集合,实现多参数查询操作,案例具体实现步骤如下。
(0)创建查询接口
package io.weew12.github.mapper;
import io.weew12.github.pojo.Customer;
import java.util.List;
import java.util.Map;
public interface CustomerMapper {
List<Customer> findByMap(Map<String, Object> map);
}(1)在映射文件CustomerMapper.xml中,添加使用<foreach>元素迭代Map集合执行批量查询操作的动态SQL,具体代码如下:
<select id="findByMap" parameterType="map" resultType="io.weew12.github.pojo.Customer">
select *
from heima_ssm_book.ch3_t_customer
where jobs = #{jobs}
and id in
<foreach collection="ids" open="(" close=")" separator="," item="id">
#{id}
</foreach>
</select>在上述配置代码中,使用<foreach>元素迭代Map集合实现客户信息的批量查询操作。由于传入参数为Map类型,所以在SQL语句中需要根据key分别获取相应的value值。例如,SQL语句中的#{jobs}获取的是Map集合中key为jobs的value值,而collection="ids"指定的是Map集合中key为ids的value值集合。
(2)为了验证上述配置,在测试类MyBatisTest中,编写测试方法findByMapTest:
/**
* - 根据客户id和工作批量查询客户信息
*/
@Test
public void findByMapTest() {
// 获取SqlSession
SqlSession sqlSession = MybatisUtils.getSession();
// 创建List集合,封装查询id
ArrayList<Integer> ids = new ArrayList<Integer>();
ids.add(1);
ids.add(2);
ids.add(3);
HashMap<String, Object> conditionMap = new HashMap<String, Object>();
conditionMap.put("ids", ids);
conditionMap.put("jobs", "teacher");
// 执行Mapper代理的查询方法,返回结果集
CustomerMapper customerMapper = sqlSession.getMapper(CustomerMapper.class);
List<Customer> byMap = customerMapper.findByMap(conditionMap);
// 输出查询结果信息
for (Customer customer : byMap) {
System.out.println(customer);
}
// 关闭SqlSession
sqlSession.close();
}在上述代码中,代码通过MyBatisUtils工具类获取SqlSession对象;创建List集合,并封装查询id;将查询条件参数封装到Map集合中;执行查询方法,并返回结果集;遍历查询到的结果集信息并输出;关闭SqlSession,释放资源。
执行MyBatisTest测试类的findByMapTest方法,控制台的输出结果如图3-14所示。

3.5 案例:学生信息查询系统
项目名称:基于MyBatis动态SQL的学生信息查询系统
项目背景:
本章学习了MyBatis动态SQL的各种元素,包括<if>、<choose>、<where>、<set>、<trim>、<foreach>等。为了巩固所学知识,现需要开发一个学生信息查询系统,通过实际编码练习动态SQL的应用。
功能需求:
功能一:动态条件查询
根据用户输入的条件,动态生成查询语句:
- 姓名优先查询:当输入的学生姓名不为空时,无论专业是否输入,都只根据学生姓名进行模糊查询
- 专业备用查询:当学生姓名为空,但专业不为空时,只根据学生专业进行模糊查询
- 无条件查询:当姓名和专业都为空时,查询所有有效学生信息(学号不为空)
功能二:固定条件查询
- 查询所有ID值小于5的学生信息。
数据表:
use heima_ssm_book;
create table ch3_t_student
(
id int primary key auto_increment comment '学生ID(主键)',
student_id varchar(20) comment '学号',
name varchar(10) comment '学生姓名',
major varchar(10) comment '专业'
);
INSERT INTO ch3_t_student (student_id, name, major)
VALUES ('2023001', '张三', '计算机科学与技术'),
('2023002', '李四', '软件工程'),
('2023003', '王五', '计算机科学与技术'),
('2023004', '赵六', '电子信息工程'),
('2023005', '钱七', '软件工程'),
('2023006', '孙八', '计算机科学与技术'),
('2023007', '周九', '网络工程'),
('2023008', '吴十', '软件工程'),
('2023009', '郑十一', '计算机科学与技术'),
('2023010', '王明', '电子信息工程');
实体:
package io.weew12.github.pojo;
public class Student {
// id
private Integer id;
// 学号
private String studentId;
// 姓名
private String name;
// 专业
private String major;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getStudentId() {
return studentId;
}
public void setStudentId(String studentId) {
this.studentId = studentId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMajor() {
return major;
}
public void setMajor(String major) {
this.major = major;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", studentId='" + studentId + '\'' +
", name='" + name + '\'' +
", major='" + major + '\'' +
'}';
}
}定义接口类和 mapper 文件:
package io.weew12.github.mapper;
public interface StudentMapper {
}<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.weew12.github.mapper.StudentMapper">
</mapper>配置文件修改加入 mapper:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="db.properties"/>
<typeAliases>
</typeAliases>
<!-- 数据库连接环境设置-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${mysql.driver}"/>
<property name="url" value="${mysql.url}"/>
<property name="username" value="${mysql.username}"/>
<property name="password" value="${mysql.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="io/weew12/github/mapper/CustomerMapper.xml"/>
<mapper resource="io/weew12/github/mapper/StudentMapper.xml"/>
</mappers>
</configuration>接口、mapperxml:
(1)单条件查询出所有id值小于5的学生的信息
接口:
package io.weew12.github.mapper;
import io.weew12.github.pojo.Student;
import java.util.List;
public interface StudentMapper {
/**
* 固定条件查询
* 查询所有ID值小于5的学生信息
*/
List<Student> findIdLessThan5();
}mapper:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.weew12.github.mapper.StudentMapper">
<select id="findIdLessThan5" parameterType="integer" resultType="io.weew12.github.pojo.Student">
select *
from heima_ssm_book.ch3_t_student
where id <![CDATA[ < ]]> 5;
</select>
</mapper>测试:
@Test
public void findIdLessThan5Test() {
// 获取SqlSession
SqlSession sqlSession = MybatisUtils.getSession();
// 获取mapper代理对象
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
List<Student> idLessThan5 = studentMapper.findIdLessThan5();
// 输出
for (Student student : idLessThan5) {
System.out.println(student);
}
// 关闭sqlSession
sqlSession.close();
}结果:

(2)多条件查询
- 当用户输入的学生姓名不为空时,则只根据学生姓名进行学生信息的查询
- 当用户输入的学生姓名为空而学生专业不为空时,则只根据学生专业进行学生信息的查询
- 当用户输入的学生姓名和专业都为空,则要求查询出所有学号不为空的学生信息
接口:
package io.weew12.github.mapper;
import io.weew12.github.pojo.Student;
import java.util.List;
public interface StudentMapper {
/**
* 多条件查询
* - 当用户输入的学生姓名不为空时,则只根据学生姓名进行学生信息的查询
* - 当用户输入的学生姓名为空而学生专业不为空时,则只根据学生专业进行学生信息的查询
* - 当用户输入的学生姓名和专业都为空,则要求查询出所有学号不为空的学生信息
*/
List<Student> findByNameOrMajorOrStudentId(Student student);
}mapper:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.weew12.github.mapper.StudentMapper">
<resultMap id="studentMap" type="io.weew12.github.pojo.Student">
<result property="studentId" column="student_id"/>
</resultMap>
<select id="findByNameOrMajorOrStudentId" parameterType="io.weew12.github.pojo.Student"
resultMap="studentMap">
select * from heima_ssm_book.ch3_t_student
<where>
<choose>
<when test="name != null and name != ''">
name = #{name}
</when>
<when test="major != null and major != ''">
major = #{major}
</when>
<otherwise>
student_id is not null;
</otherwise>
</choose>
</where>
</select>
</mapper>测试:
@Test
public void findByNameOrMajorOrStudentIdTest() {
// 获取SqlSession
SqlSession sqlSession = MybatisUtils.getSession();
// 获取mapper代理对象
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
// 构造查询条件 1 name不为空
Student student = new Student();
student.setName("王五");
// 测试1
List<Student> byNameOrMajorOrStudentId1 = studentMapper.findByNameOrMajorOrStudentId(student);
System.out.println("demo1");
byNameOrMajorOrStudentId1.forEach(System.out::println);
// 构造查询条件2 name为空 major不为空
student.setName(null);
student.setMajor("软件工程");
// 测试2
List<Student> byNameOrMajorOrStudentId2 = studentMapper.findByNameOrMajorOrStudentId(student);
System.out.println("demo2");
byNameOrMajorOrStudentId2.forEach(System.out::println);
// 构造查询条件3 name major 都为空
student.setName(null);
student.setMajor(null);
// 测试3
List<Student> byNameOrMajorOrStudentId3 = studentMapper.findByNameOrMajorOrStudentId(student);
System.out.println("demo3");
byNameOrMajorOrStudentId3.forEach(System.out::println);
// 关闭sqlSession
sqlSession.close();
}结果:

3.6 本章小结
本章主要讲解了动态SQL的相关知识。首先讲解了动态SQL中的元素;其次讲解了条件查询操作,包括<if>元素、<choose>元素、<when>元素、<otherwise>元素、<where>元素和<trim>元素的使用;然后讲解了更新操作;最后讲解了复杂查询操作。通过学习本章的内容,了解常用动态SQL元素的主要作用,并能掌握这些元素在实际开发中的应用方法。在MyBatis框架中,这些动态SQL元素十分重要,熟练掌握它们能够极大地提高开发效率。
【思考题】
1. MyBatis 动态 SQL 中的常用元素及其作用
| 元素 | 说明 |
|---|---|
<if> | 用于实现条件判断。如果test属性中的表达式为true,则该标签内的SQL语句会被添加到最终生成的SQL中。 |
<choose>、<when>、<otherwise> | 类似于Java中的switch...case语句。<choose>标签内可以包含多个<when>标签和一个可选的<otherwise>标签。当遇到第一个测试结果为真的<when>时,就使用该部分的SQL语句;如果没有一个<when>的测试结果为真,则执行<otherwise>部分的内容(如果有定义的话)。 |
<where> | 自动地处理SQL语句中WHERE子句的前置关键词AND或OR,并去除多余的空格或关键字,使得SQL更加简洁易读。 |
<set> | 主要用于更新操作,它会智能地处理SET关键字以及逗号分隔符,避免出现语法错误。 |
<trim> | 比<where>和<set>更灵活,允许用户自定义前缀、后缀以及需要移除的首尾字符串。 |
<foreach> | 遍历集合、数组等,常用于IN条件构造或批量插入/删除操作。它可以自定义开始符、结束符及分隔符。 |
<bind> | 创建一个变量并将其绑定到OGNL表达式的上下文中,方便后续引用。 |
2. 使用 <foreach> 时 collection 属性需要注意的几点
在使用 <foreach> 时,对于 collection 属性有几个关键点需要注意:
- 类型匹配:确保传递给
collection属性的对象类型与期望一致。例如,如果是 List 类型,则直接传入 List 对象;如果是 Map,则应指定 key 值。 - 参数命名:当从方法参数中获取集合时,请确保参数名称与 XML 文件中声明的一致。如果不一致,可能会导致找不到对应的集合对象。
- 索引问题:默认情况下,
<foreach>使用 index 作为循环变量名,但如果外部已经存在同名变量,则可能引起冲突。可以通过设置index属性来自定义索引变量名。 - 特殊字符处理:根据实际需求配置
open,close,separator等属性来正确处理 SQL 语句中的特殊符号,比如逗号分隔符。 - 性能考虑:虽然
<foreach>很强大,但在某些场景下(如大数据量的批量插入),直接使用 JDBC Batch 模式可能会更高效。因此,在设计应用时需权衡具体需求与性能表现。
通过合理利用这些注意事项,可以更有效地运用<foreach>完成各种复杂的 SQL 构建任务。
