在前两篇博客中,我们掌握了 MyBatis 的基础搭建、核心架构与 Mapper 代理开发,能应对简单的单表 CRUD 场景。但实际项目中,业务往往更复杂 —— 比如 “多条件动态查询”“员工与部门的关联查询”“高频查询的性能优化” 等。本篇将聚焦 MyBatis 的三大高级特性:动态 SQL(灵活拼接 SQL)、关联查询(处理多表关系)、查询缓存(提升性能),结合文档中的实战案例,帮你解决复杂业务场景,真正做到 “学以致用”。
目录
一、动态 SQL:MyBatis 的 “灵活拼接神器”
实际开发中,SQL 语句往往不是固定的 —— 比如 “查询员工” 时,用户可能输入姓名查询,也可能输入部门号查询,还可能两者都输入。如果为每种情况写一个 SQL,会导致代码冗余。MyBatis 的动态 SQL 通过标签判断条件,自动拼接 SQL,彻底解决这一问题。
1.1 动态 SQL 的核心价值
动态 SQL 的本质是 “根据参数是否为空或满足条件,动态生成合法的 SQL 语句”,避免手动拼接 SQL 的痛点:
- 无需担心 “第一个条件是 AND/OR” 导致的语法错误;
- 无需处理 “字段末尾多余逗号”(如更新操作中);
- 支持循环遍历集合(如
IN
条件),简化批量操作。
MyBatis 提供 8 种常用动态 SQL 标签,我们结合文档中的实战案例,逐一讲解核心用法。
1.2 核心动态 SQL 标签实战
(1)<if>
标签:基础条件判断
<if>
标签是动态 SQL 的基础,用于 “满足条件则拼接 SQL 片段”,常与test
属性(OGNL 表达式)配合使用。
场景:查询员工,姓名不为空则按姓名模糊查询,部门号不为空则按部门号查询。
<select id="selectEmpByCond" parameterType="emp" resultType="emp">
select * from emp where 1=1
<!-- 若ename不为空且非空字符串,拼接姓名条件 -->
<if test="ename != null and ename != ''">
and ename like concat('%', #{ename}, '%')
</if>
<!-- 若deptno不为空,拼接部门号条件 -->
<if test="deptno != null">
and deptno = #{deptno}
</if>
</select>
关键说明:
test="ename != null and ename != ''"
:判断参数ename
是否有效(非空且非空字符串);concat('%', #{ename}, '%')
:MySQL 中拼接模糊查询的%
,避免 SQL 注入(文档中强调#{}
比${}
更安全);where 1=1
:临时占位,避免 “第一个条件是 AND” 导致的语法错误(后续<where>
标签可替代这一写法)。
(2)<where>
标签:智能处理 AND/OR
<where>
标签是<if>
标签的 “好搭档”,能自动处理条件拼接中的语法问题:
- 若内部有满足条件的
<if>
,自动添加WHERE
关键字; - 自动去掉第一个条件前的
AND
或OR
; - 若内部无满足条件的
<if>
,不添加WHERE
,避免语法错误。
优化上述<if>
案例:
<select id="selectEmpByCond" parameterType="emp" resultType="emp">
select * from emp
<where>
<if test="ename != null and ename != ''">
and ename like concat('%', #{ename}, '%') <!-- 无需担心第一个条件是AND -->
</if>
<if test="deptno != null">
and deptno = #{deptno}
</if>
</where>
</select>
文档要点:<where>
标签会智能忽略条件开头的AND/OR
,且无需手动写where 1=1
,代码更简洁。
(3)<choose>-<when>-<otherwise>
标签:二选一的条件
类似 Java 的switch-case-default
,<choose>
标签下的<when>
按顺序判断,只执行第一个满足条件的<when>
,都不满足则执行<otherwise>
。
场景:查询员工,优先按薪资查(薪资≤指定值),其次按姓名查,都不满足则查部门号 = 10。
<select id="selectEmpByChoose" parameterType="emp" resultType="emp">
select * from emp
<where>
<choose>
<when test="sal != null">
sal <= #{sal} <!-- XML中“<”需转义为“<” -->
</when>
<when test="ename != null and ename != ''">
ename like concat('%', #{ename}, '%')
</when>
<otherwise>
deptno = 10 <!-- 所有条件不满足时执行 -->
</otherwise>
</choose>
</where>
</select>
文档说明:<choose>
适用于 “多个条件中只选一个” 的场景,避免<if>
标签的 “多条件同时生效” 问题。
(4)<trim>
标签:自定义前缀 / 后缀
<trim>
标签比<where>
更灵活,支持自定义 “添加前缀”“添加后缀”“覆盖首尾字符”,核心属性如下:
prefix
:给内部内容添加前缀(如WHERE
);suffix
:给内部内容添加后缀(如)
);prefixOverrides
:去掉内部内容开头的指定字符(如AND/OR
);suffixOverrides
:去掉内部内容末尾的指定字符(如,
)。
场景 1:替代<where>
标签
<trim prefix="where" prefixOverrides="and|or">
<if test="ename != null">and ename like '%${ename}%'</if>
<if test="deptno != null">and deptno = #{deptno}</if>
</trim>
场景 2:动态插入字段(处理末尾逗号)
插入操作中,若部分字段为空,会导致INSERT
语句末尾多逗号,<trim>
可自动去掉:
<insert id="insertEmp" parameterType="emp">
insert into emp
<!-- 动态拼接字段名,去掉末尾逗号 -->
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="ename != null">ename,</if>
<if test="job != null">job,</if>
<if test="sal != null">sal,</if>
</trim>
values
<!-- 动态拼接字段值,去掉末尾逗号 -->
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="ename != null">#{ename},</if>
<if test="job != null">#{job},</if>
<if test="sal != null">#{sal},</if>
</trim>
</insert>
文档要点:<trim>
标签是动态 SQL 中最灵活的标签,可应对<where>
和<set>
无法覆盖的场景。
(5)<set>
标签:动态更新的 “逗号杀手”
更新操作中,若用<if>
标签,可能出现 “字段末尾多逗号”(如update emp set ename=?,
),<set>
标签可自动去掉末尾逗号,并添加SET
关键字。
场景:动态更新员工信息,字段不为空则更新该字段。
<update id="updateEmp" parameterType="emp">
update emp
<set>
<if test="ename != null">ename = #{ename},</if>
<if test="job != null">job = #{job},</if>
<if test="sal != null">sal = #{sal},</if>
</set>
where empno = #{empno}
</update>
文档说明:<set>
标签会自动添加SET
关键字,并去掉内部内容末尾的逗号,避免update
语句语法错误。
(6)<foreach>
标签:处理集合与 IN 条件
当 SQL 需要IN
条件(如where deptno in (10,20,30)
)或批量操作时,<foreach>
标签可遍历集合生成对应 SQL 片段,核心属性如下:
collection
:集合类型(list
=List,array
= 数组,map的key
=Map 中的集合);open
:遍历开始符号(如(
);close
:遍历结束符号(如)
);item
:集合元素的别名(如deptno
);separator
:元素之间的分隔符(如,
)。
场景 1:遍历 List 集合,查询部门号在列表中的员工
<select id="selectEmpByDeptnos" parameterType="java.util.List" resultType="emp">
select * from emp
<where>
deptno in
<foreach collection="list" open="(" close=")" item="deptno" separator=",">
#{deptno}
</foreach>
</where>
</select>
场景 2:遍历数组,查询员工编号在数组中的员工
<select id="selectEmpByEmpnosArr" parameterType="int[]" resultType="emp">
select * from emp
<where>
empno in
<foreach collection="array" open="(" close=")" item="empno" separator=",">
#{empno}
</foreach>
</where>
</select>
文档要点:collection
属性需根据参数类型选择(List 用list
,数组用array
),若参数是 Map,需写 Map 中集合的key
。
(7)<sql>
片段:SQL 代码复用
若多个 SQL 有重复片段(如select empno, ename, job from emp
),可提取为<sql>
片段,避免重复书写,提升维护性。
<!-- 定义SQL片段:id为片段唯一标识 -->
<sql id="empColumns">
empno, ename, job, sal, deptno
</sql>
<!-- 引用SQL片段:用<include refid="片段id"> -->
<select id="selectEmp" resultType="emp">
select <include refid="empColumns"/> from emp
</select>
<select id="selectEmpByDeptno" parameterType="int" resultType="emp">
select <include refid="empColumns"/> from emp where deptno = #{deptno}
</select>
文档说明:<sql>
片段适用于重复的字段列表、查询条件等,减少代码冗余。
(8)<bind>
标签:跨数据库模糊查询
不同数据库的模糊查询语法不同(MySQL 用concat
,Oracle 用||
),<bind>
标签可定义变量统一语法,提升代码可移植性。
<select id="selectEmpByEname" parameterType="emp" resultType="emp">
<!-- 定义变量name:值为“%+ename+%” -->
<bind name="name" value="'%' + ename + '%'"/>
select * from emp where ename like #{name}
</select>
文档要点:<bind>
标签无需关心数据库类型,统一用#{name}
引用变量,避免因数据库切换修改 SQL。
二、关联查询:处理多表关系(一对一 / 一对多 / 多表)
实际业务中,单表查询很少见,更多是 “员工 - 部门”“部门 - 员工”“用户 - 订单 - 商品” 等多表关联场景。MyBatis 通过<resultMap>
标签的<association>
(一对一)和<collection>
(一对多)子标签,实现复杂关联映射。
2.1 关联查询的核心概念
在讲解案例前,需明确两种常见关联关系:
- 一对一:一个对象对应一个对象(如一个员工对应一个部门);
- 一对多:一个对象对应多个对象(如一个部门对应多个员工);
- 多对多:需通过中间表转换为 “一对多 + 多对一”(如用户 - 订单 - 商品,用户与商品是多对多,通过订单表关联)。
MyBatis 通过<resultMap>
定义关联规则,无需手动遍历多表结果集,自动映射为实体类对象。
2.2 一对一关联查询:员工→部门
场景:查询员工信息时,同时查询员工所属的部门信息(一个员工只属于一个部门)。
实现步骤:
定义实体类:在Emp
类中添加Dept
属性,存储关联的部门信息。
public class Emp {
private int empno;
private String ename;
private int deptno;
private Dept dept; // 一对一关联:员工所属部门
// getter/setter、toString()
}
public class Dept {
private int deptno;
private String dname;
private String loc;
// getter/setter、toString()
}
编写 Mapper 接口:定义查询方法。
public interface EmpMapper {
List<Emp> selectEmpWithDept(); // 查询员工及所属部门
}
配置 Mapper.xml:用<resultMap>
+<association>
定义关联映射。
<mapper namespace="com.jr.mapper.EmpMapper">
<!-- 定义resultMap:映射Emp与Dept的一对一关联 -->
<resultMap id="empWithDeptMap" type="emp">
<!-- 映射Emp的基本字段:id标签对应主键 -->
<id column="empno" property="empno"/>
<result column="ename" property="ename"/>
<result column="deptno" property="deptno"/>
<!-- 一对一关联Dept:用<association> -->
<association property="dept" javaType="dept">
<!-- javaType:关联实体类的类型(Dept) -->
<id column="d_deptno" property="deptno"/> <!-- 用别名避免字段名冲突 -->
<result column="dname" property="dname"/>
<result column="loc" property="loc"/>
</association>
</resultMap>
<!-- 关联查询SQL:多表连接,用别名区分字段 -->
<select id="selectEmpWithDept" resultMap="empWithDeptMap">
select e.empno, e.ename, e.deptno,
d.deptno as d_deptno, d.dname, d.loc
from emp e
inner join dept d on e.deptno = d.deptno
</select>
</mapper>
- 测试代码:
@Test
public void testSelectEmpWithDept() {
SqlSession session = factory.openSession();
EmpMapper empMapper = session.getMapper(EmpMapper.class);
List<Emp> emps = empMapper.selectEmpWithDept();
for (Emp emp : emps) {
System.out.println("员工:" + emp.getEname() + ",部门:" + emp.getDept().getDname());
}
session.close();
}
文档要点:<association>
标签用于一对一关联,javaType
属性指定关联实体类的类型,需用别名避免多表字段名冲突(如d.deptno as d_deptno
)。
2.3 一对多关联查询:部门→员工
场景:查询部门信息时,同时查询部门下的所有员工(一个部门有多个员工)。
实现步骤:
定义实体类:在Dept
类中添加List<Emp>
属性,存储关联的员工列表。
public class Dept {
private int deptno;
private String dname;
private String loc;
private List<Emp> emps; // 一对多关联:部门下的员工列表
// getter/setter、toString()
}
编写 Mapper 接口:
public interface DeptMapper {
Dept selectDeptWithEmp(int deptno); // 查询部门及下属员工
}
配置 Mapper.xml:用<resultMap>
+<collection>
定义一对多关联。
<mapper namespace="com.jr.mapper.DeptMapper">
<!-- 定义resultMap:映射Dept与Emp的一对多关联 -->
<resultMap id="deptWithEmpMap" type="dept">
<!-- 映射Dept的基本字段 -->
<id column="deptno" property="deptno"/>
<result column="dname" property="dname"/>
<result column="loc" property="loc"/>
<!-- 一对多关联Emp列表:用<collection> -->
<collection property="emps" ofType="emp">
<!-- ofType:集合中元素的类型(Emp) -->
<id column="e_empno" property="empno"/> <!-- 别名避免冲突 -->
<result column="e_ename" property="ename"/>
<result column="e_sal" property="sal"/>
</collection>
</resultMap>
<!-- 关联查询SQL:左连接查询部门与员工 -->
<select id="selectDeptWithEmp" parameterType="int" resultMap="deptWithEmpMap">
select d.deptno, d.dname, d.loc,
e.empno as e_empno, e.ename as e_ename, e.sal as e_sal
from dept d
left join emp e on d.deptno = e.deptno
where d.deptno = #{deptno}
</select>
</mapper>
- 测试代码:
@Test
public void testSelectDeptWithEmp() {
SqlSession session = factory.openSession();
DeptMapper deptMapper = session.getMapper(DeptMapper.class);
Dept dept = deptMapper.selectDeptWithEmp(10);
System.out.println("部门:" + dept.getDname());
for (Emp emp : dept.getEmps()) {
System.out.println(" 员工:" + emp.getEname() + ",薪资:" + emp.getSal());
}
session.close();
}
文档要点:<collection>
标签用于一对多关联,ofType
属性指定集合元素的类型(区别于javaType
,javaType
用于指定属性类型,如List
)。
2.4 多表关联查询:用户→订单→订单详情→商品
场景:查询用户信息时,同时查询用户的所有订单、每个订单的详情、每个详情对应的商品(多表关联:用户 1:N 订单 1:N 订单详情 1:1 商品)。
实现步骤:
定义实体类:逐层关联(Users→Orders→OrderDetail→Items)。
// 用户类:1个用户对应多个订单
public class Users {
private int uid;
private String uname;
private List<Orders> orders; // 一对多关联订单
// getter/setter
}
// 订单类:1个订单对应多个详情
public class Orders {
private int oid;
private String orderid;
private List<OrderDetail> orderdetails; // 一对多关联详情
// getter/setter
}
// 订单详情类:1个详情对应1个商品
public class OrderDetail {
private int odid;
private int itemsnum;
private Items item; // 一对一关联商品
// getter/setter
}
// 商品类
public class Items {
private int iid;
private String name;
private double price;
// getter/setter
}
配置 Mapper.xml:嵌套<collection>
和<association>
实现多表映射。
<mapper namespace="com.jr.mapper.UserMapper">
<!-- 多表关联resultMap:用户→订单→详情→商品 -->
<resultMap id="userOrderDetailItemMap" type="users">
<id column="uid" property="uid"/>
<result column="uname" property="uname"/>
<!-- 1:N:用户→订单 -->
<collection property="orders" ofType="orders">
<id column="oid" property="oid"/>
<result column="orderid" property="orderid"/>
<!-- 1:N:订单→订单详情 -->
<collection property="orderdetails" ofType="orderdetail">
<id column="odid" property="odid"/>
<result column="itemsnum" property="itemsnum"/>
<!-- 1:1:订单详情→商品 -->
<association property="item" javaType="items">
<id column="iid" property="iid"/>
<result column="name" property="name"/>
<result column="price" property="price"/>
</association>
</collection>
</collection>
</resultMap>
<!-- 多表关联SQL:四表连接 -->
<select id="selectUserWithAll" resultMap="userOrderDetailItemMap">
select u.uid, u.uname,
o.oid, o.orderid,
od.odid, od.itemsnum,
i.iid, i.name, i.price
from users u
inner join orders o on u.uid = o.userid
inner join orderdetail od on o.orderid = od.orderid
inner join items i on od.itemid = i.iid
</select>
</mapper>
文档要点:多表关联需嵌套使用<collection>
(一对多)和<association>
(一对一),确保每层映射的column
与property
对应。
三、查询缓存:MyBatis 的 “性能优化利器”
缓存是 “以空间换时间” 的优化手段,MyBatis 提供两级缓存,减少数据库访问次数,提升高频查询的性能。
3.1 缓存的核心概念
MyBatis 的缓存分为两级,作用域和生命周期不同:
- 一级缓存:
SqlSession
级别(本地缓存),默认开启,无需配置; - 二级缓存:
Mapper
(namespace)级别(全局缓存),默认关闭,需手动配置; - 第三方缓存:如 Ehcache、Redis,用于分布式场景(多服务共享缓存)。
3.2 一级缓存:SqlSession 级别的本地缓存
核心特点:
- 作用域:同一个
SqlSession
(从openSession()
到close()
); - 实现:基于
PerpetualCache
(HashMap)存储; - 失效场景:
- 调用
SqlSession.close()
; - 调用
SqlSession.commit()
/rollback()
(事务提交 / 回滚会清空缓存); - 执行相同 ID 的
insert
/update
/delete
(修改数据会清空缓存,避免脏读); - 调用
SqlSession.clearCache()
(手动清空缓存)。
- 调用
实战案例:
@Test
public void testFirstLevelCache() {
SqlSession session = factory.openSession();
EmpMapper empMapper = session.getMapper(EmpMapper.class);
// 第一次查询:执行SQL,结果存入一级缓存
Emp emp1 = empMapper.selectEmpByNo(7369);
System.out.println(emp1);
// 第二次查询:同一SqlSession,相同SQL,从缓存获取(不执行SQL)
Emp emp2 = empMapper.selectEmpByNo(7369);
System.out.println(emp2);
session.close();
}
日志输出:仅第一次查询执行 SQL,第二次从缓存获取。
3.3 二级缓存:Mapper 级别的全局缓存
核心特点:
- 作用域:同一个
Mapper
(namespace),跨SqlSession
共享; - 存储:默认存储序列化后的 Java 对象(需实体类实现
Serializable
接口); - 配置步骤:需开启全局开关 + Mapper 单独配置。
配置步骤:
开启全局二级缓存(SqlMapConfig.xml
):
<settings>
<setting name="cacheEnabled" value="true"/> <!-- 全局开关,默认true可省略 -->
</settings>
在 Mapper.xml 中开启二级缓存:
<mapper namespace="com.jr.mapper.EmpMapper">
<!-- 开启二级缓存:默认使用PerpetualCache -->
<cache/>
<!-- 或配置缓存参数(如过期时间、最大容量) -->
<!--
<cache
eviction="LRU" // 淘汰策略(LRU:最近最少使用)
flushInterval="60000" // 60秒刷新一次缓存
size="1024" // 最多缓存1024个对象
readOnly="true"/> // 只读模式(返回对象引用,性能高)
-->
<select id="selectEmpByNo" parameterType="int" resultType="emp">
select * from emp where empno = #{empno}
</select>
</mapper>
实体类实现 Serializable 接口:
public class Emp implements Serializable { // 二级缓存需序列化
private static final long serialVersionUID = 1L; // 序列化ID
// 字段、getter/setter
}
实战案例:
@Test
public void testSecondLevelCache() {
// 第一个SqlSession:查询后关闭,将数据刷入二级缓存
SqlSession session1 = factory.openSession();
EmpMapper empMapper1 = session1.getMapper(EmpMapper.class);
Emp emp1 = empMapper1.selectEmpByNo(7369);
System.out.println(emp1);
session1.close(); // 关闭SqlSession,一级缓存数据刷入二级缓存
// 第二个SqlSession:从二级缓存获取数据(不执行SQL)
SqlSession session2 = factory.openSession();
EmpMapper empMapper2 = session2.getMapper(EmpMapper.class);
Emp emp2 = empMapper2.selectEmpByNo(7369);
System.out.println(emp2);
session2.close();
}
文档要点:二级缓存需通过session.close()
或session.commit()
将一级缓存数据刷入,否则无法共享。
关键配置:
- 禁用二级缓存:对实时性要求高的查询(如秒杀商品库存),添加
useCache="false"
:
<select id="selectEmpByNo" parameterType="int" resultType="emp" useCache="false">
select * from emp where empno = #{empno}
</select>
- 刷新二级缓存:执行
insert
/update
/delete
后,默认清空二级缓存(避免脏读),可通过flushCache="false"
关闭(不推荐):
<update id="updateEmp" parameterType="emp" flushCache="true"> <!-- 默认true,可省略 -->
update emp set ename = #{ename} where empno = #{empno}
</update>
3.4 整合第三方缓存:Ehcache(分布式场景)
默认二级缓存是 “本地缓存”,分布式部署时(多台服务器)缓存不共享,需整合分布式缓存框架(如 Ehcache)。
配置步骤:
添加 Maven 依赖:
<!-- MyBatis-Ehcache整合包 -->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.0.2</version>
</dependency>
<!-- Ehcache核心包 -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.1</version>
</dependency>
添加 Ehcache 配置文件(ehcache.xml):
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
<!-- 缓存数据存储路径(磁盘) -->
<diskStore path="D:/mybatis-ehcache"/>
<!-- 默认缓存配置 -->
<defaultCache
maxElementsInMemory="1000" <!-- 内存最大缓存对象数 -->
eternal="false" <!-- 不永久缓存 -->
timeToIdleSeconds="120" <!-- 120秒未访问则过期 -->
timeToLiveSeconds="120" <!-- 120秒后过期 -->
overflowToDisk="true"/> <!-- 内存满时写入磁盘 -->
</ehcache>
在 Mapper.xml 中指定 Ehcache 缓存:
<mapper namespace="com.jr.mapper.EmpMapper">
<!-- 启用Ehcache缓存 -->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
<!-- SQL语句... -->
</mapper>
文档要点:Ehcache 支持内存 + 磁盘存储,分布式部署时可配置集群,实现缓存共享。
四、总结:MyBatis 核心能力回顾与实践建议
至此,MyBatis 从入门到精通系列三篇博客已全部完成,我们系统覆盖了 MyBatis 的核心能力:
- 基础层:框架概念、环境搭建(普通项目 + Maven);
- 核心层:三层架构、全局配置、Mapper 代理开发;
- 高级层:动态 SQL、关联查询、查询缓存。
实践建议:
- 动态 SQL:优先用
<where>``<set>
标签简化条件拼接,复杂场景用<trim>
,避免手动写where 1=1
; - 关联查询:一对一用
<association>
,一对多用<collection>
,多表关联需注意字段别名冲突; - 缓存优化:一级缓存默认开启,二级缓存按需开启(适合查询多、修改少的场景),分布式项目整合 Ehcache/Redis;
- 开发规范:坚持 “Mapper 代理开发”,SQL 集中在 XML 中,通过
<sql>
片段复用代码,提升维护性。
MyBatis 的核心优势在于 “轻量、灵活、解耦”—— 既保留了 SQL 的灵活性,又简化了数据映射与连接管理。掌握这些核心能力后,你不仅能应对企业级项目的持久层开发,更能在面试中从容应对 MyBatis 的高频考点(如动态 SQL、缓存机制、关联查询)。后续可进一步学习 MyBatis-Plus(MyBatis 的增强工具,简化 CRUD),但建议先夯实 MyBatis 基础,再逐步拓展。