SqlSessionFactory与SqlSession
导学
通过本节学习,你将能够:
- 区分 SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession 三者的职责边界
- 掌握三者的正确作用域:方法级、应用级、请求级
- 理解 SqlSession 的线程不安全性及资源释放规范
- 写出符合生产标准的 SqlSession 获取与关闭代码
定义
它们分别是什么
| 对象 | 职责 | 类比 |
|---|---|---|
| SqlSessionFactoryBuilder | 读取配置流,构建 SqlSessionFactory | 建筑工人(盖完楼即离开) |
| SqlSessionFactory | 创建 SqlSession 的工厂,持有全局配置 | 楼盘工厂(全局唯一,持续生产) |
| SqlSession | 与数据库交互的会话,执行 SQL 语句 | 单次看房专车(每次请求一辆,用完归还) |
解决了什么痛点
在 JDBC 原始写法中,开发者需要手动处理以下问题:
- 连接管理混乱:每次操作都新建连接还是复用连接?代码中散落着
DriverManager.getConnection() - 线程安全问题:多个线程共用一个 Connection 会导致并发异常
- 资源泄漏风险:忘记
close()会造成连接池耗尽,系统崩溃
MyBatis 通过三层对象的设计,将这些问题分层隔离:
- Builder 层负责一次性解析配置,避免重复解析 XML 的开销
- Factory 层负责全局缓存配置和连接池,保证应用内统一视图
- Session 层负责单次请求的完整生命周期,天然与请求线程绑定
核心原理
生命周期与作用域时序图
时序图解读:
- 应用启动时:Builder 一次性解析 XML,创建 Factory 后立即被垃圾回收
- 每次请求时:从 Factory 获取新的 Session,Session 从连接池借用 Connection
- 请求结束时:Session 关闭,Connection 归还连接池,Session 内部一级缓存清空
作用域规范
| 对象 | 推荐作用域 | 原因 | 错误做法的后果 |
|---|---|---|---|
| SqlSessionFactoryBuilder | 方法作用域(局部变量) | 只需创建一次 Factory,之后无存在价值 | 长期持有浪费内存,且配置变更后无法重新加载 |
| SqlSessionFactory | 应用作用域(单例/静态) | 创建代价高,包含解析后的全量配置和连接池 | 多次创建导致连接池重复初始化,连接数暴涨 |
| SqlSession | 请求/方法作用域 | 线程不安全,持有数据库连接和一级缓存 | 多线程共用导致并发修改异常;长期不关闭导致连接池耗尽 |
完整示例
场景说明
乐途公司学生管理系统需要查询学生信息。本节展示如何正确管理 SqlSessionFactory 和 SqlSession 的生命周期,包括单例工厂的实现和 try-with-resources 的规范用法。
操作前的数据库表结构及初始数据
| id | name | age | major | score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 95.50 |
| 2 | 白歌 | 21 | 软件工程 | 88.00 |
| 3 | 小崔 | 20 | 计算机科学 | 92.00 |
| 4 | 黄俪 | 21 | 信息安全 | 90.50 |
| 5 | 李眉 | 22 | 软件工程 | 87.00 |
单例 SqlSessionFactory 工具类
package com.fly.util;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
/**
* MyBatis 工具类:应用级单例管理 SqlSessionFactory
*
* 设计要点:
* 1. 静态代码块初始化,保证类加载时即完成 Factory 创建
* 2. SqlSessionFactoryBuilder 在创建完 Factory 后即被释放(方法作用域)
* 3. SqlSessionFactory 作为静态变量全局唯一(应用作用域)
*/
public class MyBatisUtil {
private static SqlSessionFactory sqlSessionFactory;
static {
String resource = "mybatis-config.xml";
try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
// Builder 是局部变量,build() 后即被回收
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
System.out.println("【初始化】SqlSessionFactory 创建成功,应用级单例");
} catch (IOException e) {
throw new RuntimeException("MyBatis 初始化失败: " + e.getMessage(), e);
}
}
/**
* 获取全局唯一的 SqlSessionFactory
*/
public static SqlSessionFactory getSqlSessionFactory() {
return sqlSessionFactory;
}
// 禁止实例化
private MyBatisUtil() {}
}
SqlSession 的 5 种 openSession() 重载
// 方式1:默认开启事务,手动提交
SqlSession session = factory.openSession();
// 方式2:指定是否自动提交
SqlSession session = factory.openSession(true); // 自动提交
SqlSession session = factory.openSession(false); // 手动提交(默认)
// 方式3:指定连接隔离级别
SqlSession session = factory.openSession(Connection.TRANSACTION_READ_COMMITTED);
// 方式4:指定是否自动提交 + 隔离级别
SqlSession session = factory.openSession(Connection.TRANSACTION_READ_COMMITTED, true);
// 方式5:指定 Executor 类型(SIMPLE/REUSE/BATCH)
SqlSession session = factory.openSession(ExecutorType.BATCH);
规范的 CRUD 操作代码
package com.fly.test;
import com.fly.entity.Student;
import com.fly.mapper.StudentMapper;
import com.fly.util.MyBatisUtil;
import org.apache.ibatis.session.SqlSession;
public class SessionLifecycleDemo {
/**
* 查询操作:try-with-resources 自动关闭 Session
*/
public Student findStudentById(int id) {
// SqlSession 是方法作用域,每次请求新建
try (SqlSession session = MyBatisUtil.getSqlSessionFactory().openSession()) {
StudentMapper mapper = session.getMapper(StudentMapper.class);
Student student = mapper.findById(id);
System.out.println("【查询结果】id=" + id + " => " + student.getName());
return student;
} // Session 自动关闭,Connection 归还连接池
}
/**
* 插入操作:需要手动提交事务
*/
public int insertStudent(Student student) {
try (SqlSession session = MyBatisUtil.getSqlSessionFactory().openSession()) {
StudentMapper mapper = session.getMapper(StudentMapper.class);
int rows = mapper.insert(student);
// 非自动提交模式下,必须手动 commit
session.commit();
System.out.println("【插入成功】影响行数: " + rows + ", 新学生: " + student.getName());
return rows;
}
}
/**
* 错误示范:将 SqlSession 作为实例变量(线程不安全!)
*/
// private SqlSession badSession = MyBatisUtil.getSqlSessionFactory().openSession(); // 严禁!
}
实际执行结果
查询执行:
【初始化】SqlSessionFactory 创建成功,应用级单例
【查询结果】id=1 => 大翔
==> Preparing: SELECT id, name, age, major, score FROM student WHERE id = ?
==> Parameters: 1(Integer)
<== Columns: id, name, age, major, score
<== Row: 1, 大翔, 22, 计算机科学, 95.50
<== Total: 1
插入执行(插入新学生 "赵六"):
【插入成功】影响行数: 1, 新学生: 赵六
==> Preparing: INSERT INTO student (name, age, major, score) VALUES (?, ?, ?, ?)
==> Parameters: 赵六(String), 23(Integer), 人工智能(String), 91.00(BigDecimal)
<== Updates: 1
插入后数据状态:
| id | name | age | major | score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 95.50 |
| 2 | 白歌 | 21 | 软件工程 | 88.00 |
| 3 | 小崔 | 20 | 计算机科学 | 92.00 |
| 4 | 黄俪 | 21 | 信息安全 | 90.50 |
| 5 | 李眉 | 22 | 软件工程 | 87.00 |
| 6 | 赵六 | 23 | 人工智能 | 91.00 |
易错场景 / 常见误区
| 误区 | 错误代码/做法 | 后果 | 正解 |
|---|---|---|---|
| SqlSessionFactoryBuilder 长期持有 | 将 Builder 设为静态变量,每次 builder.build() | 内存泄漏,配置无法热更新 | Builder 用完即丢,局部变量 |
| SqlSessionFactory 多次创建 | 每次请求都 new SqlSessionFactoryBuilder().build() | 连接池重复初始化,连接数暴涨 | 应用级单例,静态持有 |
| SqlSession 作为实例/静态变量 | private SqlSession session = factory.openSession() | 多线程并发异常,连接长期占用 | 方法级局部变量,try-with-resources |
| 忘记提交事务 | 插入后未调用 session.commit() | 数据回滚,看似成功实则未写入 | 非自动提交模式下必须手动 commit |
| 异常时未回滚 | catch 块中未 session.rollback() | 脏数据残留,数据不一致 | catch 中调用 rollback,finally 中 close |
| 关闭顺序错误 | 先关 Factory 再关 Session | 无意义,Factory 是应用级 | 只关 Session,Factory 不关 |
面试考点
Q1:SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession 的作用域分别是什么?为什么要这样设计?
A: SqlSessionFactoryBuilder 是方法作用域,因为它只需一次性解析配置创建 Factory,之后无存在价值,长期持有浪费内存。SqlSessionFactory 是应用作用域(单例),创建代价高且包含连接池,全局唯一可避免重复初始化。SqlSession 是请求/方法作用域,它线程不安全且持有数据库连接,每次请求新建一个,用完立即关闭,否则会导致连接池耗尽和并发异常。
Q2:SqlSession 是线程安全的吗?如果必须在多线程环境共享怎么办?
A: SqlSession 不是线程安全的。它内部持有数据库连接和一级缓存(HashMap),多线程并发操作会导致数据混乱和 JDBC 连接异常。正确做法是每个线程/每次请求独立获取自己的 SqlSession。如果必须在多线程环境共享数据,应使用 SqlSessionFactory 为每个线程创建独立的 Session,或使用 Spring 等框架的事务管理器来绑定线程与 Session。
Q3:openSession() 和 openSession(true) 有什么区别?生产环境推荐哪种?
A:
openSession()默认创建手动提交模式的 Session,DML 操作后必须调用session.commit(),适合需要事务控制的业务场景。openSession(true)开启自动提交,每条 SQL 执行后立即提交,无法保证事务原子性,仅适用于简单查询或无事务要求的场景。生产环境推荐手动提交模式,由业务层控制事务边界,确保数据一致性。
Q4:SqlSession 的三种 ExecutorType 有什么区别?
A:
SIMPLE(默认)为每条语句创建新的 PreparedStatement。REUSE复用 PreparedStatement,减少编译开销,适合同一会话中重复执行相同结构的 SQL。BATCH将多条修改语句批量发送给数据库,减少网络往返,适合大批量插入/更新场景,但此时 select 操作可能返回不正确结果。
小结
本节深入讲解了 MyBatis 运行时最核心的两个对象:SqlSessionFactory 和 SqlSession。通过生命周期时序图,我们理解了 Builder 的 "用过即丢"、Factory 的 "全局唯一"、Session 的 "用完即关" 三大原则。完整的工具类和 CRUD 示例展示了生产级代码的写法,而易错场景表则警示了线程安全和资源泄漏的陷阱。
关键记忆点:
- Builder = 方法级,Factory = 应用级单例,Session = 请求级 try-with-resources
- SqlSession 线程不安全,严禁作为实例变量或静态变量
- 非自动提交模式下,DML 必须手动 commit,异常时 rollback
- 连接池中的 Connection 由 Session 借用,Session.close() 即归还
下一章引子
现在我们已经有了全局唯一的 SqlSessionFactory,也知道了如何正确打开和关闭 SqlSession。但 Session 拿到之后,具体怎么执行 SQL?实体类怎么写?Mapper 接口和 XML 怎么对应?下一节将带你完成第一个完整的 MyBatis 程序——从实体类、Mapper 接口、Mapper XML 到测试代码,手把手走完 CRUD 全流程,并展示真实的控制台 SQL 输出。