本文 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,减少无效查询
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐