SqlSession核心方法
导学
通过本节学习,你将能够:
- 掌握 SqlSession 提供的全部查询、更新、事务控制核心方法
- 理解
selectOne、selectList、selectMap、selectCursor的适用场景与差异 - 熟练使用
RowBounds逻辑分页和ResultHandler自定义结果处理 - 写出包含事务提交、回滚、异常处理的完整 DML 代码
- 区分直接调用 SqlSession API 与 Mapper 接口代理两种编程模式
定义
SqlSession 是什么
SqlSession 是 MyBatis 中与数据库交互的核心会话对象,它封装了 JDBC 的 Connection、Statement、ResultSet 等底层操作,为开发者提供了一组简洁的 Java API 来执行 SQL 语句。
与 JDBC 原始写法的对比
在 JDBC 原始写法中,即使是一次简单的查询,也需要编写大量样板代码:
// JDBC 原始写法:查询单条学生记录
public Student findById_JDBC(int id) {
String sql = "SELECT id, name, age, major, score FROM student WHERE id = ?";
try (Connection conn = DriverManager.getConnection(url, user, password);
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
Student stu = new Student();
stu.setId(rs.getInt("id"));
stu.setName(rs.getString("name"));
stu.setAge(rs.getInt("age"));
stu.setMajor(rs.getString("major"));
stu.setScore(rs.getBigDecimal("score"));
return stu;
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return null;
}
使用 SqlSession 后,同样的功能只需一行:
Student student = session.selectOne("com.fly.mapper.StudentMapper.findById", 1);
| 维度 | JDBC 原始写法 | SqlSession API |
|---|---|---|
| 代码量 | 20+ 行,样板代码占比高 | 1 行,聚焦业务逻辑 |
| 资源管理 | 手动管理 Connection/Statement/ResultSet | 框架自动托管,try-with-resources 即可 |
| 结果映射 | 逐列 rs.getXxx() 手动赋值 | 自动反射映射到实体属性 |
| 异常转换 | 捕获 SQLException,抛出自定义异常 | 统一转换为 PersistenceException 体系 |
| 缓存支持 | 无 | 内置一级缓存,同 Session 重复查询自动命中 |
核心原理
SqlSession 方法调用时序图
时序图解读:
- 入口拦截:
selectOne并非直接查库,而是先通过statementId定位MappedStatement - 缓存优先:每次查询先检查 SqlSession 级别的一级缓存,命中则直接返回,避免重复访问数据库
- 执行链路:Executor 负责调度,StatementHandler 负责创建和执行 JDBC Statement,ParameterHandler 负责参数绑定,ResultSetHandler 负责结果映射
- 唯一性校验:
selectOne在返回前会检查结果集大小,若超过 1 条立即抛出TooManyResultsException,这是它比selectList多一层保护的原因 - 缓存回填:查询结果写入一级缓存,同一会话内再次执行相同语句直接命中
完整方法速查表
查询类方法
| 方法签名 | 返回值 | 说明 | 可能抛出的异常 |
|---|---|---|---|
selectOne(String statement, Object parameter) | T | 查询单条记录,结果必须为 0 或 1 条 | TooManyResultsException(结果 > 1 条) |
selectList(String statement, Object parameter) | List<E> | 查询多条记录返回 List,结果为空返回空 List 而非 null | 无特殊异常 |
selectList(String statement, Object parameter, RowBounds rowBounds) | List<E> | 带逻辑分页的查询,内存分页而非 SQL 分页 | 无特殊异常 |
selectMap(String statement, Object parameter, String mapKey) | Map<K, V> | 查询结果转为 Map,指定哪一列作为 Map 的 key | 无特殊异常 |
selectCursor(String statement, Object parameter) | Cursor<T> | 游标查询,惰性逐条读取,适合大数据量 | IllegalStateException(游标已关闭) |
select(String statement, Object parameter, ResultHandler handler) | void | 自定义结果处理器,逐条处理结果集,无返回值 | 无特殊异常 |
更新类方法
| 方法签名 | 返回值 | 说明 | 可能抛出的异常 |
|---|---|---|---|
insert(String statement, Object parameter) | int | 执行插入语句,返回影响行数 | PersistenceException(SQL 错误) |
update(String statement, Object parameter) | int | 执行更新语句,返回影响行数 | PersistenceException(SQL 错误) |
delete(String statement, Object parameter) | int | 执行删除语句,返回影响行数 | PersistenceException(SQL 错误) |
事务控制方法
| 方法签名 | 返回值 | 说明 |
|---|---|---|
commit() | void | 提交当前事务,默认 force=false |
commit(boolean force) | void | 强制提交事务,忽略脏数据判断 |
rollback() | void | 回滚当前事务 |
rollback(boolean force) | void | 强制回滚事务 |
clearCache() | void | 清空当前 SqlSession 的一级缓存 |
其他方法
| 方法签名 | 返回值 | 说明 |
|---|---|---|
getMapper(Class<T> type) | T | 获取 Mapper 接口的 JDK 动态代理对象 |
getConfiguration() | Configuration | 获取全局配置对象,可读取所有 MappedStatement |
close() | void | 关闭 Session,归还数据库连接到连接池 |
完整示例
操作前的数据库表结构及初始数据
CREATE TABLE student (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20),
age INT,
major VARCHAR(20),
score DECIMAL(5,2)
);
INSERT INTO student (name, age, major, score) VALUES
('大翔', 22, '计算机科学', 95.5),
('白歌', 21, '软件工程', 88.0),
('小崔', 20, '计算机科学', 92.0),
('黄俪', 21, '信息安全', 90.5),
('李眉', 22, '软件工程', 87.0);
初始数据状态:
| 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 |
场景一:selectOne 查询单条学生记录(展示结果唯一性异常)
需求:通过 ID 查询单条学生记录,并演示当 SQL 返回多条时抛出的异常。
StudentMapper.xml 中的语句定义:
<!-- 根据 ID 查询单个学生 -->
<select id="findById" resultType="com.fly.entity.Student">
SELECT id, name, age, major, score FROM student WHERE id = #{id}
</select>
<!-- 根据专业查询(可能返回多条) -->
<select id="findByMajor" resultType="com.fly.entity.Student">
SELECT id, name, age, major, score FROM student WHERE major = #{major}
</select>
测试代码:
package com.fly.test;
import com.fly.entity.Student;
import com.fly.util.MyBatisUtil;
import org.apache.ibatis.exceptions.TooManyResultsException;
import org.apache.ibatis.session.SqlSession;
public class SelectOneDemo {
public static void main(String[] args) {
// 1. 正确使用 selectOne:查询 ID=1 的大翔
System.out.println("========== 1. selectOne 正确查询单条 ==========");
try (SqlSession session = MyBatisUtil.getSqlSessionFactory().openSession()) {
String statement = "com.fly.mapper.StudentMapper.findById";
Student student = session.selectOne(statement, 1);
System.out.println("查询结果: " + student);
}
// 2. 错误使用 selectOne:查询专业为"计算机科学"的学生(实际有2条)
System.out.println("\n========== 2. selectOne 结果不唯一,抛出异常 ==========");
try (SqlSession session = MyBatisUtil.getSqlSessionFactory().openSession()) {
String statement = "com.fly.mapper.StudentMapper.findByMajor";
try {
// 计算机科学专业有大翔(id=1)和小崔(id=3),共2条记录
Student student = session.selectOne(statement, "计算机科学");
System.out.println("查询结果: " + student);
} catch (TooManyResultsException e) {
System.err.println("【异常捕获】TooManyResultsException: " + e.getMessage());
System.err.println("【原因】selectOne 要求结果必须为 0 或 1 条,实际返回了多条");
System.err.println("【正解】预期可能返回多条时,应使用 selectList");
}
}
}
}
实际执行结果:
========== 1. selectOne 正确查询单条 ==========
==> 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
查询结果: Student{id=1, name='大翔', age=22, major='计算机科学', score=95.50}
========== 2. selectOne 结果不唯一,抛出异常 ==========
==> Preparing: SELECT id, name, age, major, score FROM student WHERE major = ?
==> Parameters: 计算机科学(String)
<== Columns: id, name, age, major, score
<== Row: 1, 大翔, 22, 计算机科学, 95.50
<== Row: 3, 小崔, 20, 计算机科学, 92.00
<== Total: 2
【异常捕获】TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 2
【原因】selectOne 要求结果必须为 0 或 1 条,实际返回了多条
【正解】预期可能返回多条时,应使用 selectList
结果分析:
- 场景 1 中
id=1唯一对应大翔,selectOne正常返回 - 场景 2 中 "计算机科学" 专业匹配大翔和小崔两条记录,
selectOne在ResultSetHandler完成映射后立即校验结果数,发现 2 条时抛出TooManyResultsException - 该异常在
DefaultSqlSession中抛出,早于业务代码拿到结果,有效防止了数据不一致导致的隐蔽 bug
场景二:selectList + RowBounds 逻辑分页(展示内存分页的 SQL 输出)
需求:查询所有学生,但只取第 2 条开始的 2 条记录(即第 3、4 条),展示 RowBounds 的内存分页行为。
关键认知:RowBounds 是逻辑分页(内存分页),MyBatis 仍然会将全量数据查询到内存,然后在结果集层面跳过和截取,而非在 SQL 层面拼接 LIMIT。
StudentMapper.xml 中的语句定义:
<!-- 查询所有学生 -->
<select id="findAll" resultType="com.fly.entity.Student">
SELECT id, name, age, major, score FROM student
</select>
测试代码:
package com.fly.test;
import com.fly.entity.Student;
import com.fly.util.MyBatisUtil;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.SqlSession;
import java.util.List;
public class RowBoundsDemo {
public static void main(String[] args) {
System.out.println("========== 1. 不带 RowBounds:查询全部 5 条 ==========");
try (SqlSession session = MyBatisUtil.getSqlSessionFactory().openSession()) {
String statement = "com.fly.mapper.StudentMapper.findAll";
List<Student> allList = session.selectList(statement, null);
System.out.println("总记录数: " + allList.size());
allList.forEach(s -> System.out.println(" " + s));
}
System.out.println("\n========== 2. 带 RowBounds:offset=2, limit=2 ==========");
System.out.println("【注意】SQL 中不会出现 LIMIT,全量数据仍会从 MySQL 返回");
try (SqlSession session = MyBatisUtil.getSqlSessionFactory().openSession()) {
String statement = "com.fly.mapper.StudentMapper.findAll";
// offset=2 表示跳过前 2 条,limit=2 表示最多取 2 条
RowBounds rowBounds = new RowBounds(2, 2);
List<Student> pageList = session.selectList(statement, null, rowBounds);
System.out.println("分页后记录数: " + pageList.size());
pageList.forEach(s -> System.out.println(" " + s));
}
System.out.println("\n========== 3. 带 RowBounds:offset=0, limit=3 ==========");
try (SqlSession session = MyBatisUtil.getSqlSessionFactory().openSession()) {
String statement = "com.fly.mapper.StudentMapper.findAll";
RowBounds rowBounds = new RowBounds(0, 3);
List<Student> pageList = session.selectList(statement, null, rowBounds);
System.out.println("分页后记录数: " + pageList.size());
pageList.forEach(s -> System.out.println(" " + s));
}
}
}
实际执行结果:
========== 1. 不带 RowBounds:查询全部 5 条 ==========
==> Preparing: SELECT id, name, age, major, score FROM student
==> Parameters:
<== Columns: id, name, age, major, score
<== Row: 1, 大翔, 22, 计算机科学, 95.50
<== Row: 2, 白歌, 21, 软件工程, 88.00
<== Row: 3, 小崔, 20, 计算机科学, 92.00
<== Row: 4, 黄俪, 21, 信息安全, 90.50
<== Row: 5, 李眉, 22, 软件工程, 87.00
<== Total: 5
总记录数: 5
Student{id=1, name='大翔', age=22, major='计算机科学', score=95.50}
Student{id=2, name='白歌', age=21, major='软件工程', score=88.00}
Student{id=3, name='小崔', age=20, major='计算机科学', score=92.00}
Student{id=4, name='黄俪', age=21, major='信息安全', score=90.50}
Student{id=5, name='李眉', age=22, major='软件工程', score=87.00}
========== 2. 带 RowBounds:offset=2, limit=2 ==========
【注意】SQL 中不会出现 LIMIT,全量数据仍会从 MySQL 返回
==> Preparing: SELECT id, name, age, major, score FROM student
==> Parameters:
<== Columns: id, name, age, major, score
<== Row: 1, 大翔, 22, 计算机科学, 95.50
<== Row: 2, 白歌, 21, 软件工程, 88.00
<== Row: 3, 小崔, 20, 计算机科学, 92.00
<== Row: 4, 黄俪, 21, 信息安全, 90.50
<== Row: 5, 李眉, 22, 软件工程, 87.00
<== Total: 5
分页后记录数: 2
Student{id=3, name='小崔', age=20, major='计算机科学', score=92.00}
Student{id=4, name='黄俪', age=21, major='信息安全', score=90.50}
========== 3. 带 RowBounds:offset=0, limit=3 ==========
==> Preparing: SELECT id, name, age, major, score FROM student
==> Parameters:
<== Columns: id, name, age, major, score
<== Row: 1, 大翔, 22, 计算机科学, 95.50
<== Row: 2, 白歌, 21, 软件工程, 88.00
<== Row: 3, 小崔, 20, 计算机科学, 92.00
<== Row: 4, 黄俪, 21, 信息安全, 90.50
<== Row: 5, 李眉, 22, 软件工程, 87.00
<== Total: 5
分页后记录数: 3
Student{id=1, name='大翔', age=22, major='计算机科学', score=95.50}
Student{id=2, name='白歌', age=21, major='软件工程', score=88.00}
Student{id=3, name='小崔', age=20, major='计算机科学', score=92.00}
结果分析:
| 对比项 | 不带 RowBounds | 带 RowBounds(offset=2, limit=2) |
|---|---|---|
| SQL 输出 | SELECT ... FROM student | SELECT ... FROM student(无 LIMIT) |
| 数据库返回行数 | 5 条 | 5 条 |
| MyBatis 处理后的行数 | 5 条 | 2 条(跳过前 2,取 2 条) |
| 实际返回的数据 | 全部 | 小崔、黄俪 |
重要结论:RowBounds 是内存分页,不是 SQL 分页。数据量较大时,应使用插件(如 PageHelper)或手动在 SQL 中拼接 LIMIT 实现物理分页,避免全量数据加载到内存造成 OOM。
场景三:insert + commit/rollback 事务控制(展示事务提交和回滚前后的数据对比)
需求:向 student 表插入两名新学生,第一次正常提交,第二次故意制造异常后回滚,对比数据库的最终状态。
StudentMapper.xml 中的语句定义:
<!-- 插入学生 -->
<insert id="insertStudent" useGeneratedKeys="true" keyProperty="id">
INSERT INTO student (name, age, major, score)
VALUES (#{name}, #{age}, #{major}, #{score})
</insert>
测试代码:
package com.fly.test;
import com.fly.entity.Student;
import com.fly.util.MyBatisUtil;
import org.apache.ibatis.session.SqlSession;
import java.math.BigDecimal;
import java.util.List;
public class TransactionDemo {
/**
* 查询并打印当前所有学生
*/
private static void printAllStudents(SqlSession session) {
String statement = "com.fly.mapper.StudentMapper.findAll";
List<Student> list = session.selectList(statement);
System.out.println("当前共 " + list.size() + " 条记录:");
list.forEach(s -> System.out.println(" " + s));
}
public static void main(String[] args) {
// ========== 事务 1:正常提交 ==========
System.out.println("========== 事务 1:插入新学生并提交 ==========");
try (SqlSession session = MyBatisUtil.getSqlSessionFactory().openSession()) {
String statement = "com.fly.mapper.StudentMapper.insertStudent";
// 插入学生 "赵六"
Student zhaoliu = new Student("赵六", 23, "人工智能", new BigDecimal("91.00"));
int rows1 = session.insert(statement, zhaoliu);
System.out.println("【插入赵六】影响行数: " + rows1 + ", 生成主键: " + zhaoliu.getId());
// 插入学生 "孙七"
Student sunqi = new Student("孙七", 24, "数据科学", new BigDecimal("89.50"));
int rows2 = session.insert(statement, sunqi);
System.out.println("【插入孙七】影响行数: " + rows2 + ", 生成主键: " + sunqi.getId());
// 提交事务
session.commit();
System.out.println("【事务提交】数据已持久化到数据库");
// 在同一个 Session 中查询验证
printAllStudents(session);
}
// ========== 事务 2:故意异常后回滚 ==========
System.out.println("\n========== 事务 2:插入后回滚 ==========");
SqlSession session = null;
try {
session = MyBatisUtil.getSqlSessionFactory().openSession();
String statement = "com.fly.mapper.StudentMapper.insertStudent";
// 插入学生 "周八"
Student zhouba = new Student("周八", 25, "网络安全", new BigDecimal("93.00"));
int rows3 = session.insert(statement, zhouba);
System.out.println("【插入周八】影响行数: " + rows3 + ", 生成主键: " + zhouba.getId());
// 模拟业务异常:比如后续校验发现该学生不符合入学条件
if (zhouba.getAge() > 24) {
throw new RuntimeException("【业务异常】年龄超过 24 岁,不符合入学条件");
}
session.commit();
} catch (Exception e) {
System.err.println(e.getMessage());
if (session != null) {
session.rollback();
System.err.println("【事务回滚】周八的插入操作已被撤销");
}
} finally {
if (session != null) {
session.close();
}
}
// ========== 最终验证:开启新 Session 查询数据库真实状态 ==========
System.out.println("\n========== 最终验证:数据库真实状态 ==========");
try (SqlSession session = MyBatisUtil.getSqlSessionFactory().openSession()) {
printAllStudents(session);
}
}
}
实际执行结果:
========== 事务 1:插入新学生并提交 ==========
==> Preparing: INSERT INTO student (name, age, major, score) VALUES (?, ?, ?, ?)
==> Parameters: 赵六(String), 23(Integer), 人工智能(String), 91.00(BigDecimal)
<== Updates: 1
【插入赵六】影响行数: 1, 生成主键: 6
==> Preparing: INSERT INTO student (name, age, major, score) VALUES (?, ?, ?, ?)
==> Parameters: 孙七(String), 24(Integer), 数据科学(String), 89.50(BigDecimal)
<== Updates: 1
【插入孙七】影响行数: 1, 生成主键: 7
【事务提交】数据已持久化到数据库
==> Preparing: SELECT id, name, age, major, score FROM student
==> Parameters:
<== Columns: id, name, age, major, score
<== Row: 1, 大翔, 22, 计算机科学, 95.50
<== Row: 2, 白歌, 21, 软件工程, 88.00
<== Row: 3, 小崔, 20, 计算机科学, 92.00
<== Row: 4, 黄俪, 21, 信息安全, 90.50
<== Row: 5, 李眉, 22, 软件工程, 87.00
<== Row: 6, 赵六, 23, 人工智能, 91.00
<== Row: 7, 孙七, 24, 数据科学, 89.50
<== Total: 7
当前共 7 条记录:
Student{id=1, name='大翔', age=22, major='计算机科学', score=95.50}
Student{id=2, name='白歌', age=21, major='软件工程', score=88.00}
Student{id=3, name='小崔', age=20, major='计算机科学', score=92.00}
Student{id=4, name='黄俪', age=21, major='信息安全', score=90.50}
Student{id=5, name='李眉', age=22, major='软件工程', score=87.00}
Student{id=6, name='赵六', age=23, major='人工智能', score=91.00}
Student{id=7, name='孙七', age=24, major='数据科学', score=89.50}
========== 事务 2:插入后回滚 ==========
==> Preparing: INSERT INTO student (name, age, major, score) VALUES (?, ?, ?, ?)
==> Parameters: 周八(String), 25(Integer), 网络安全(String), 93.00(BigDecimal)
<== Updates: 1
【插入周八】影响行数: 1, 生成主键: 8
【业务异常】年龄超过 24 岁,不符合入学条件
【事务回滚】周八的插入操作已被撤销
========== 最终验证:数据库真实状态 ==========
==> Preparing: SELECT id, name, age, major, score FROM student
==> Parameters:
<== Columns: id, name, age, major, score
<== Row: 1, 大翔, 22, 计算机科学, 95.50
<== Row: 2, 白歌, 21, 软件工程, 88.00
<== Row: 3, 小崔, 20, 计算机科学, 92.00
<== Row: 4, 黄俪, 21, 信息安全, 90.50
<== Row: 5, 李眉, 22, 软件工程, 87.00
<== Row: 6, 赵六, 23, 人工智能, 91.00
<== Row: 7, 孙七, 24, 数据科学, 89.50
<== Total: 7
当前共 7 条记录:
Student{id=1, name='大翔', age=22, major='计算机科学', score=95.50}
Student{id=2, name='白歌', age=21, major='软件工程', score=88.00}
Student{id=3, name='小崔', age=20, major='计算机科学', score=92.00}
Student{id=4, name='黄俪', age=21, major='信息安全', score=90.50}
Student{id=5, name='李眉', age=22, major='软件工程', score=87.00}
Student{id=6, name='赵六', age=23, major='人工智能', score=91.00}
Student{id=7, name='孙七', age=24, major='数据科学', score=89.50}
结果分析:
| 阶段 | 操作 | 数据库状态 | 说明 |
|---|---|---|---|
| 初始 | 无 | 5 条(大翔~李眉) | 基础数据 |
| 事务 1 | 插入赵六、孙七,commit | 7 条 | 事务提交,数据持久化 |
| 事务 2 | 插入周八,业务异常,rollback | 仍为 7 条 | 回滚撤销了周八的插入 |
| 最终 | 新 Session 查询 | 7 条 | 赵六、孙七保留,周八未出现 |
关键观察:
insert返回的影响行数为 1,但不 commit 数据不会真正写入数据库- 同一个 SqlSession 中,未 commit 的插入可以通过
selectList查到(读未提交,一级缓存),但其他 Session 查不到 rollback()会撤销当前事务中所有的 DML 操作,包括已生成的主键值在数据库层面也会被回收- 生产代码中务必在
catch块中调用rollback(),在finally块中调用close()
易错场景 / 常见误区
| 误区 | 错误代码/做法 | 后果 | 正解 |
|---|---|---|---|
| selectOne 用于可能返回多条的查询 | session.selectOne("findByMajor", "计算机科学") | 抛出 TooManyResultsException | 预期可能多条时,使用 selectList |
| 混淆 RowBounds 为物理分页 | 认为 SQL 会自动拼接 LIMIT | 全量数据加载到内存,大数据量时 OOM | 大数据量分页使用 PageHelper 或手动 SQL LIMIT |
| DML 后忘记 commit | session.insert(...) 后直接关闭 | 控制台显示 Updates: 1,但数据库无数据 | 非自动提交模式下必须手动 session.commit() |
| 异常时未 rollback | catch 块中只有日志,无 rollback | 部分数据已提交,部分未提交,数据不一致 | catch 中调用 session.rollback() |
| 多线程共用 SqlSession | 将 Session 作为实例变量共享 | 并发修改一级缓存,JDBC 连接异常 | 每个线程/每次请求独立获取 Session |
| selectCursor 使用后未关闭 | 遍历完 Cursor 未关闭 Session | 数据库连接和游标资源泄漏 | 在 try-with-resources 中使用 Cursor |
| 用 selectMap 时 mapKey 不唯一 | mapKey 指定的列有重复值 | 后覆盖前,Map 中数据丢失 | 确保 mapKey 列值全局唯一,或使用 Map<String, List<T>> |
面试考点
Q1:selectOne 和 selectList 有什么区别?什么情况下 selectOne 会抛异常?
A:
selectOne用于查询单条记录,内部调用selectList后校验结果集大小:若为 0 条返回 null,若为 1 条返回该对象,若 超过 1 条则抛出TooManyResultsException。selectList则返回List<E>,结果为空时返回空 List(非 null),不会校验结果数量。因此当 SQL 条件可能匹配多条记录时(如按专业查询),必须使用selectList。
Q2:RowBounds 分页是物理分页还是逻辑分页?生产环境为什么不推荐直接使用它?
A:
RowBounds是逻辑分页(内存分页)。MyBatis 仍然向数据库发送不带LIMIT的原始 SQL,数据库返回全量数据后,MyBatis 在ResultSetHandler层面通过skip和limit截取部分结果。这意味着数据库 IO 和网络传输没有减少,大数据量时全量数据进入 JVM 内存,极易导致 OOM。生产环境推荐物理分页:使用 MySQL 的LIMIT语法,或借助 PageHelper 等分页插件自动改写 SQL。
Q3:SqlSession 的 insert、update、delete 返回的 int 代表什么?是否等于主键值?
A: 返回的
int代表 JDBC 的影响行数(affected rows),即数据库中被插入、更新、删除的记录条数,不是主键值。主键值通过useGeneratedKeys="true"和keyProperty="id"配置,由 MyBatis 在插入后向数据库索取LAST_INSERT_ID()并回填到传入的实体对象中,通过student.getId()获取。
Q4:同一个 SqlSession 中,insert 后未 commit,为什么 selectList 能查到刚插入的数据?其他 Session 却查不到?
A: 同一个 SqlSession 内,
insert后数据首先进入 SqlSession 一级缓存。该 Session 后续的selectList会优先命中一级缓存,因此能"看到"未提交的数据(类似于读未提交)。但其他 SqlSession 拥有独立的一级缓存,且数据库层面事务未提交,所以其他 Session 查不到。这恰恰说明了一级缓存的 Session 隔离性,也警示了长事务中脏读的风险。
小结
本节系统讲解了 SqlSession 的全部核心方法,覆盖查询、更新、事务控制三大类别。通过 selectOne 的异常场景,我们理解了结果唯一性校验的重要性;通过 RowBounds 的内存分页实验,我们认清了逻辑分页与物理分页的本质差异;通过 insert + commit/rollback 的事务演示,我们掌握了 DML 操作的标准事务边界写法。
关键记忆点:
selectOne结果 > 1 条时抛TooManyResultsException,预期多条请用selectListRowBounds是内存分页,SQL 中无LIMIT,大数据量场景避免使用insert/update/delete返回影响行数,主键通过useGeneratedKeys回填实体- DML 必须
commit()才持久化,异常时rollback(),资源在finally中close() - 一级缓存是 SqlSession 级别的,Session 关闭即清空,多线程严禁共享 Session
下一章引子
SqlSession 的 API 虽然功能完备,但直接写 session.selectOne("com.fly.mapper.StudentMapper.findById", 1) 存在字符串硬编码、类型不安全、IDE 无法自动补全等问题。MyBatis 提供的 getMapper(Class<T> type) 方法可以获取 Mapper 接口的代理对象,让我们以 mapper.findById(1) 这种类型安全、可维护的方式操作数据库。下一节将深入讲解 Mapper 接口与映射方式,对比 XML 映射与注解映射的语法差异和适用场景。