乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 MyBatis概述与快速上手

    • 本章定位
    • MyBatis简介
    • 环境搭建
    • 第一个MyBatis程序
    • SqlSessionFactoryBuilder与openSession重载
    • SqlSessionFactory与SqlSession
    • SqlSession核心方法
    • 不使用 XML 构建 SqlSessionFactory
    • Mapper接口与映射方式
    • Java API 目录结构
  • 第2章 全局配置文件详解

    • 本章定位
    • properties
    • settings
    • typeAliases
    • typeHandlers
    • objectFactory
    • plugins
    • environments
    • transactionManager
    • dataSource
    • databaseIdProvider
    • mappers
    • 日志配置
  • 第3章 SQL映射文件基础

    • 本章定位
    • select
    • insert
    • update
    • delete
    • 参数传递与占位符
    • 主键生成策略
    • resultType
    • resultMap
    • 自动映射详解
    • sql片段
    • SQL 语句构建器
  • 第4章 动态SQL

    • 本章定位
    • if
    • choose、when、otherwise
    • where
    • set
    • foreach
    • trim
    • bind
    • script 元素:在注解映射器中启用动态 SQL
    • _databaseId 与动态 SQL 的多数据库支持
    • 动态 SQL 中插入脚本语言
  • 第5章 结果映射与关联查询

    • 本章定位
    • resultMap详解
    • association
    • collection
    • discriminator
    • N+1查询问题
    • 延迟加载
  • 第6章 MyBatis注解开发

    • 本章定位
    • @Select
    • @Insert
    • @Update
    • @Delete
    • @Param
    • @Options
    • @SelectKey
    • @Results
    • @Result
    • @One
    • @Many
    • @SelectProvider
  • 第7章 缓存与性能优化

    • 本章定位
    • 一级缓存
    • 二级缓存
    • 缓存配置详解
    • 自定义缓存
    • Executor执行器类型
    • 分页插件

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 方法调用时序图

时序图解读:

  1. 入口拦截:selectOne 并非直接查库,而是先通过 statementId 定位 MappedStatement
  2. 缓存优先:每次查询先检查 SqlSession 级别的一级缓存,命中则直接返回,避免重复访问数据库
  3. 执行链路:Executor 负责调度,StatementHandler 负责创建和执行 JDBC Statement,ParameterHandler 负责参数绑定,ResultSetHandler 负责结果映射
  4. 唯一性校验:selectOne 在返回前会检查结果集大小,若超过 1 条立即抛出 TooManyResultsException,这是它比 selectList 多一层保护的原因
  5. 缓存回填:查询结果写入一级缓存,同一会话内再次执行相同语句直接命中

完整方法速查表

查询类方法

方法签名返回值说明可能抛出的异常
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);

初始数据状态:

idnameagemajorscore
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 studentSELECT ... 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插入赵六、孙七,commit7 条事务提交,数据持久化
事务 2插入周八,业务异常,rollback仍为 7 条回滚撤销了周八的插入
最终新 Session 查询7 条赵六、孙七保留,周八未出现

关键观察:

  1. insert 返回的影响行数为 1,但不 commit 数据不会真正写入数据库
  2. 同一个 SqlSession 中,未 commit 的插入可以通过 selectList 查到(读未提交,一级缓存),但其他 Session 查不到
  3. rollback() 会撤销当前事务中所有的 DML 操作,包括已生成的主键值在数据库层面也会被回收
  4. 生产代码中务必在 catch 块中调用 rollback(),在 finally 块中调用 close()

易错场景 / 常见误区

误区错误代码/做法后果正解
selectOne 用于可能返回多条的查询session.selectOne("findByMajor", "计算机科学")抛出 TooManyResultsException预期可能多条时,使用 selectList
混淆 RowBounds 为物理分页认为 SQL 会自动拼接 LIMIT全量数据加载到内存,大数据量时 OOM大数据量分页使用 PageHelper 或手动 SQL LIMIT
DML 后忘记 commitsession.insert(...) 后直接关闭控制台显示 Updates: 1,但数据库无数据非自动提交模式下必须手动 session.commit()
异常时未 rollbackcatch 块中只有日志,无 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,预期多条请用 selectList
  • RowBounds 是内存分页,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 映射与注解映射的语法差异和适用场景。

上一页
SqlSessionFactory与SqlSession
下一页
不使用 XML 构建 SqlSessionFactory