MyBatis 执行流程与关联映射
本文 MyBatis 涵盖核心执行流程、动态代理机制、多表关联映射(嵌套查询)以及延迟加载原理。
一、MyBatis 核心执行流程
MyBatis 的执行流程可以概括为四步:
读取配置文件 → 生成 SqlSessionFactory → 创建 SqlSession → 操作数据库
1. 读取配置文件
MyBatis 启动时,SqlSessionFactoryBuilder 负责读取两类配置文件:
sqlMapConfig.xml:全局配置,包含数据源、事务管理、别名、mapper注册等mapper.xml(或接口注解):各模块的 SQL 映射文件
2. 生成 SqlSessionFactory
SqlSessionFactoryBuilder 解析完配置文件后,生成 SqlSessionFactory 对象。
关键特性:
- 创建开销极大(解析XML、初始化连接池、加载所有映射关系)
- 线程安全
- 整个应用只需创建一次,推荐使用单例模式管理
// 工具类:保证 factory 全局只创建一次
public class MybatisUtils {
private static SqlSessionFactory factory;
static {
try {
InputStream is = Resources.getResourceAsStream("sqlMapConfig.xml");
factory = new SqlSessionFactoryBuilder().build(is);
} catch (Exception e) {
e.printStackTrace();
}
}
public static SqlSession getSession() {
return factory.openSession();
}
}
3. 创建 SqlSession
SqlSession 是用户与数据库交互的核心通道,由 SqlSessionFactory.openSession() 创建。
关键特性:
- 创建开销小(轻量级对象)
- 线程不安全,不能在多线程间共享
- 生命周期为一次请求或一个方法,用完必须关闭
| 对象 | 作用域 | 线程安全 | 创建开销 |
|---|---|---|---|
SqlSessionFactory | 应用级(全局单例) | ✅ 安全 | 大 |
SqlSession | 请求/方法级 | ❌ 不安全 | 小 |
4. 操作数据库
SqlSession 通过动态代理生成 Mapper 接口的代理对象,将方法调用转发给底层 JDBC 执行。
二、动态代理机制
调用 session.getMapper(StudentMapper.class) 时,MyBatis 并不需要你写接口的实现类,而是在运行时动态生成代理对象。
StudentMapper 代理对象 → 内部持有 → SqlSession 引用
↓
转发给 SqlSession 执行 SQL
代理对象的生命周期跟随 SqlSession,Session 关闭后 mapper 代理对象也不可再用。这就是为什么说 Mapper 是 SqlSession 级别的。
SqlSession session1 = factory.openSession();
SqlSession session2 = factory.openSession();
// 两个不同的 session,各自持有独立的代理对象
StudentMapper mapper1 = session1.getMapper(StudentMapper.class);
StudentMapper mapper2 = session2.getMapper(StudentMapper.class);
三、关联映射:多表查询
MyBatis 提供两种方式处理多表关联:嵌套结果映射和嵌套查询(分步查询)。本文重点讲后者,因为它支持延迟加载。
场景
学生表和老师表,一个老师对应多个学生,查询学生时同时获取对应的老师信息。
表设计:
CREATE TABLE teacher (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20)
);
CREATE TABLE student (
id INT PRIMARY KEY AUTO_INCREMENT,
sname VARCHAR(20),
age INT,
sex VARCHAR(5),
t_id INT -- 外键,指向 teacher.id
);
实体类:
public class Student implements Serializable {
private int id;
private String sname;
private int age;
private String sex;
private int t_id;
private Teacher teacher; // 关联的老师对象
// getter/setter/toString ...
}
public class Teacher implements Serializable {
private int id;
private String name;
// getter/setter/toString ...
}
嵌套查询配置
StudentMapper.xml:
<mapper namespace="com.demo.mapper.StudentMapper">
<!-- 第一条 SQL:查所有学生 -->
<select id="findAllStudents" resultMap="StudentTeacherMap">
select * from student
</select>
<resultMap id="StudentTeacherMap" type="Student">
<result property="id" column="id"/>
<result property="sname" column="sname"/>
<result property="age" column="age"/>
<result property="sex" column="sex"/>
<result property="t_id" column="t_id"/>
<!--
association:处理关联对象
select → 调用 TeacherMapper 中的 getTeacherById
column → 把当前学生的 t_id 值作为参数传过去
-->
<association property="teacher"
select="com.demo.mapper.TeacherMapper.getTeacherById"
column="t_id"
javaType="Teacher"/>
</resultMap>
</mapper>
TeacherMapper.xml:
<mapper namespace="com.demo.mapper.TeacherMapper">
<!-- 第二条 SQL:根据 id 查老师 -->
<select id="getTeacherById" resultType="Teacher">
select * from teacher where id = #{id}
</select>
</mapper>
数据流动过程
第一步:select * from student
↓ 返回结果,每行有 t_id 字段
第二步:association 读取 column="t_id" 的值
↓ 把该值传给 getTeacherById
第三步:select * from teacher where id = #{t_id的值}
↓ 查出 Teacher 对象
第四步:MyBatis 自动把 Teacher 对象塞入 Student.teacher 字段
易混淆点:
<result>里的column是"从数据库哪列读数据",<association>里的column是"把哪列的值当参数传出去",两者含义不同。
最终返回的对象结构
Student {
id = 1,
sname = "张三",
age = 18,
sex = "男",
t_id = 1,
teacher = Teacher { id=1, name="王老师" } // 嵌套查询填入
}
四、延迟加载(Lazy Loading)
为什么需要延迟加载
嵌套查询默认会立即执行两条 SQL。如果查询了 100 个学生,就会额外发出 100 条查老师的 SQL,即使页面上根本不展示老师信息,这是严重浪费。
延迟加载的策略是:用到才查,不用不查。
// 只打印学生姓名,不访问 teacher 字段
// 开启延迟加载后,查老师的 SQL 不会执行
for (Student s : students) {
System.out.println(s.getSname());
}
// 访问了 teacher 字段,此时才触发第二条 SQL
System.out.println(students.get(0).getTeacher().getName());
开启方式
在 sqlMapConfig.xml 中添加:
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
为什么嵌套查询才能延迟加载
延迟加载的前提是"两条 SQL 相互独立",MyBatis 才能控制第二条 SQL 的执行时机。如果是一条 JOIN 语句,数据已经全部返回,无法再做延迟处理。
五、完整 Demo
项目结构
src/
├── main/
│ ├── java/com/demo/
│ │ ├── entity/ Student.java, Teacher.java
│ │ ├── mapper/ StudentMapper.java, TeacherMapper.java
│ │ └── utils/ MybatisUtils.java
│ └── resources/
│ ├── sqlMapConfig.xml
│ └── mapper/ StudentMapper.xml, TeacherMapper.xml
└── test/
└── java/ StudentTest.java
测试类
public class StudentTest {
private SqlSession session;
private StudentMapper mapper;
@Before
public void setUp() {
session = MybatisUtils.getSession();
mapper = session.getMapper(StudentMapper.class);
}
@After
public void tearDown() {
session.close();
}
@Test
public void testFindAllStudents() {
List<Student> students = mapper.findAllStudents();
for (Student s : students) {
System.out.println("学生:" + s.getSname()
+ " → 老师:" + s.getTeacher().getName());
}
}
}
预期输出:
学生:张三 → 老师:王老师
学生:李四 → 老师:李老师
学生:王五 → 老师:王老师
学生:赵六 → 老师:张老师
学生:钱七 → 老师:李老师
总结
| 知识点 | |
|---|---|
| SqlSessionFactory | 重量级,全局单例,应用启动时创建一次 |
| SqlSession | 轻量级,线程不安全,用完即关 |
| 动态代理 | Mapper 代理对象内部持有 SqlSession,生命周期一致 |
| 嵌套查询 | 两条独立 SQL,column 传参连接两条查询 |
| 延迟加载 | 访问关联字段时才触发第二条 SQL,减少无效查询 |
更多推荐



所有评论(0)