《JAVA面经实录》- MyBatis 框架面试题

一、MyBatis 是什么?优缺点?

1.面试标准回答

MyBatis 是一款开源的半自动ORM框架,基于Java持久层技术,核心作用是简化JDBC开发,将SQL语句与Java代码解耦,通过XML或注解的方式配置SQL语句,实现Java对象与数据库表的映射(ORM),底层封装了JDBC,开发者无需手动处理数据库连接、Statement创建、结果集封装等重复工作,专注于SQL编写和业务逻辑实现。

MyBatis 不属于全自动化ORM框架(如Hibernate),它不自动生成SQL,而是由开发者手动编写SQL,兼顾了灵活性和开发效率,是目前企业中使用最广泛的持久层框架(常与Spring、Spring Boot整合)。

核心优点

  • 灵活性高:SQL语句完全由开发者控制,可灵活优化SQL(如索引、关联查询优化),支持动态SQL、存储过程、自定义函数,适配复杂业务场景;

  • 解耦性强:将SQL语句与Java代码分离(XML配置或注解),便于维护和修改,降低代码耦合度;

  • 学习成本低:核心是SQL映射,熟悉SQL和Java即可快速上手,配置简单,源码相对简洁,易于理解;

  • 轻量级框架:核心包体积小,无过多依赖,运行效率高,不占用过多系统资源;

  • 支持多种映射方式:支持XML配置映射、注解映射,支持一对一、一对多、多对多关联映射;

  • 扩展性强:支持插件机制(如分页插件PageHelper),可自定义拦截器,扩展MyBatis功能。

核心缺点

  • SQL编写工作量大:所有SQL语句需开发者手动编写,尤其是复杂关联查询,耗时较长;

  • 数据库移植性差:SQL语句与数据库方言绑定(如MySQL的limit、Oracle的rownum),切换数据库时,需修改大量SQL语句;

  • 无内置缓存(仅一级缓存):二级缓存需手动配置(如结合Redis、EHCache),缓存功能相对简单,需手动维护;

  • 无对象状态管理:所有Java对象都是瞬时态或脱管态,修改对象后需手动调用update方法,或通过SqlSession提交,才会同步到数据库;

  • 调试难度大:SQL语句编写错误时,调试起来相对繁琐,需结合日志排查问题。

2.面试补充

MyBatis 与 MyBatis-Plus 区别:MyBatis-Plus是MyBatis的增强工具,在MyBatis基础上封装了基础CRUD操作(BaseMapper),无需编写基础SQL,同时保留了MyBatis的灵活性,支持分页、条件构造器等功能,目前企业使用最广泛。

二、#{} 和 ${} 区别?为什么推荐 #{}?

1.面试标准回答

#{} 和 ${} 是MyBatis中两种核心的参数占位符,均用于在SQL语句中注入参数,但底层实现、安全性、适用场景有本质区别,这是面试高频必考题,核心区别如下:

对比维度

#{}(推荐使用)

${}(谨慎使用)

底层实现

采用预编译SQL方式,将#{}替换为?占位符,MyBatis会使用PreparedStatement执行SQL,参数通过setXXX()方法注入,避免SQL注入。

采用字符串拼接方式,直接将参数值拼接在SQL语句中,不进行预编译,参数值会直接替换${},存在SQL注入风险。

安全性

安全,可有效防止SQL注入(如参数为恶意SQL片段,会被当作普通字符串处理)。

不安全,存在SQL注入风险(如参数为 "1 or 1=1",拼接后会改变SQL逻辑)。

参数类型处理

自动处理参数类型(如字符串自动添加单引号,数字不添加),无需手动拼接引号。

不处理参数类型,需手动拼接引号(如字符串参数需手动加''),否则会报错。

适用场景

绝大多数场景,尤其是参数注入(如查询、新增、修改、删除的条件参数、字段值)。

仅适用于动态拼接SQL片段(如表名、字段名、排序方式),无法用#{}替代的场景。

示例

SQL:select * from user where id = #{id};参数id=1,最终执行SQL:select * from user where id = ?(预编译后注入参数1)。

SQL:select * from ${tableName} where id = ${id};参数tableName=user、id=1,最终执行SQL:select * from user where id = 1(字符串拼接)。

2.为什么推荐使用 #{}?

核心原因有两点,也是企业开发的核心要求:

  1. 安全性:#{} 采用预编译SQL方式,能有效防止SQL注入攻击,这是企业级应用中最关键的安全需求(如用户登录、数据查询等场景,避免恶意参数篡改SQL逻辑);

  2. 便捷性:#{} 自动处理参数类型,无需手动拼接引号、转换参数格式,减少SQL编写错误,提升开发效率。

3.实战避坑

1. 禁止用 ${} 处理用户输入的参数(如登录密码、查询条件),否则会引发SQL注入漏洞;

2. 若必须使用 ${}(如动态表名),需对参数进行严格校验(如白名单校验,确保参数是允许的表名、字段名),避免恶意参数注入;

3. 特殊场景:MyBatis中like查询,若用#{},需手动拼接%(如 like concat('%', #{name}, '%')),若用${}(不推荐),可直接写 like '%${name}%',但需注意安全。

三、MyBatis 一级缓存、二级缓存机制

1.面试标准回答

MyBatis 提供两级缓存机制,核心目的是减少数据库访问次数,提升查询性能,缓存的核心是将频繁查询的数据存入内存,后续查询直接从内存获取,避免重复发送SQL。两级缓存的作用范围、生命周期、管理方式各不相同,具体如下:

(1). 一级缓存(SqlSession 级缓存,默认开启,无法关闭)

  • 作用范围:当前 SqlSession 内有效,仅在单个 SqlSession 中共享,SqlSession 关闭、提交或回滚后,一级缓存中的数据会被清空。

  • 核心作用:缓存当前 SqlSession 中查询到的结果集,避免同一 SqlSession 内多次查询同一数据(如多次调用同一个 Mapper 方法,参数相同,仅第一次发送SQL)。

  • 底层实现:基于 SqlSession 内置的 HashMap 集合,key 是由「MappedStatement 的 id + 查询参数 + 分页信息 + 绑定的 SQL 语句」组成的唯一标识,value 是查询结果集(Java 对象)。

  • 核心特性:

    • 一级缓存是 MyBatis 内置的,无需任何配置,默认开启;

    • 当 SqlSession 执行 insert、update、delete 操作时,会自动清空一级缓存(避免缓存与数据库数据不一致);

    • 同一 SqlSession 内,若查询参数、SQL 语句完全一致,会直接从缓存获取数据,不发送 SQL。

  • 示例: 

    SqlSession session = sqlSessionFactory.openSession();
    UserMapper mapper = session.getMapper(UserMapper.class);
    
    // 第一次查询,发送SQL,存入一级缓存
    User user1 = mapper.selectById(1);
    // 第二次查询,参数相同,从一级缓存获取,不发送SQL
    User user2 = mapper.selectById(1);
    
    session.close(); // 关闭SqlSession,一级缓存清空

(2). 二级缓存(SqlSessionFactory 级缓存,默认关闭,需手动开启)

  • 作用范围:整个应用程序,由 SqlSessionFactory 管理,所有 SqlSession 共享二级缓存中的数据,SqlSessionFactory 销毁后,二级缓存数据才会销毁。

  • 核心作用:缓存频繁查询、修改较少的全局数据(如字典表、配置表),减少不同 SqlSession 查询同一数据时的数据库访问。

  • 开启方式(三步):

    • 第一步:在 MyBatis 核心配置文件(mybatis-config.xml)中开启二级缓存: 

      <settings>
        <setting name="cacheEnabled" value="true"/&gt; <!-- 开启二级缓存,默认false -->
      </settings>

    • 第二步:在 Mapper 映射文件(如 UserMapper.xml)中配置二级缓存(标注该 Mapper 的缓存生效): 

      <mapper namespace="com.xxx.mapper.UserMapper"&gt;
        &lt;cache/&gt; <!-- 开启当前Mapper的二级缓存,使用默认配置 -->
        <!-- 可选:自定义缓存配置(如缓存大小、过期时间) -->
        <cache size="1024" eviction="LRU" flushInterval="60000"/>
      </mapper>

    • 第三步:确保查询的实体类实现 Serializable 接口(二级缓存需要序列化对象,避免缓存时抛出异常): 

       public class User implements Serializable { // 实现Serializable接口
        private Integer id;
        private String name;
        // getter/setter 省略
      }

  • 底层实现:默认使用 MyBatis 内置的 PerpetualCache(基于 HashMap),也可集成第三方缓存(如 Redis、EHCache),通过配置 cache-ref 或自定义缓存实现类替换默认缓存。

  • 核心特性:

    • 二级缓存依赖一级缓存:查询数据时,先查询一级缓存 → 再查询二级缓存 → 最后查询数据库;

    • 当任何一个 SqlSession 执行 insert、update、delete 操作时,会自动清空当前 Mapper 的二级缓存(确保缓存与数据库一致性);

    • 可通过 @CacheNamespace 注解(注解方式)替代 XML 中的 <cache/> 配置。

2.面试补充(高频追问)

(1). 一级缓存与二级缓存的关系:二级缓存的作用范围比一级缓存大,多个 SqlSession 可共享二级缓存;数据查询时,优先从一级缓存获取,一级缓存没有再从二级缓存获取,都没有才查询数据库;

(2). 二级缓存的禁用:可在单个查询标签中配置 useCache="false"(如 <select useCache="false">),禁止该查询使用二级缓存;

(3). 第三方缓存集成:MyBatis 支持集成 Redis、EHCache 等第三方缓存,只需实现 Cache 接口,配置自定义缓存类即可(如集成 Redis,需导入 mybatis-redis 依赖,配置 cache 标签的 type 属性为 Redis 缓存实现类)。

四、缓存失效场景有哪些?

(1).面试标准回答

MyBatis 一级缓存和二级缓存的失效场景,核心都是「缓存与数据库数据不一致」,MyBatis 会自动清空缓存,避免脏数据读取。以下是常见的缓存失效场景,分一级缓存和二级缓存分别说明,覆盖源码级细节和实战场景:

(一)、一级缓存失效场景(5种,核心是 SqlSession 状态变化或数据修改)

  1. SqlSession 关闭后失效:一级缓存是 SqlSession 级的,SqlSession.close() 会清空当前 SqlSession 的一级缓存,再次获取 SqlSession 查询同一数据,会重新发送 SQL。

  2. SqlSession 提交/回滚后失效:执行 session.commit() 或 session.rollback() 后,MyBatis 会自动清空一级缓存(避免提交/回滚后,缓存数据与数据库不一致)。

  3. 执行 insert、update、delete 操作后失效:无论是否提交事务,只要在当前 SqlSession 中执行了 insert、update、delete 操作,MyBatis 会立即清空一级缓存(因为这些操作会修改数据库数据,缓存数据已过时)。

  4. 查询参数或 SQL 语句不同:一级缓存的 key 由 MappedStatement id + 查询参数 + SQL 语句等组成,若参数不同、SQL 语句不同(如排序方式不同),会认为是不同的查询,不命中缓存,重新发送 SQL。

  5. 手动清空缓存:调用 sqlSession.clearCache() 方法,会手动清空当前 SqlSession 的一级缓存,后续查询需重新发送 SQL。

(二)、二级缓存失效场景(6种,核心是 Mapper 数据修改或缓存配置不当)

  1. 未开启二级缓存:未在 mybatis-config.xml 中配置 cacheEnabled="true",或未在 Mapper 映射文件中添加 <cache/> 标签,二级缓存不生效,自然不存在缓存失效。

  2. 实体类未实现 Serializable 接口:二级缓存需要序列化对象,若实体类未实现 Serializable,会抛出序列化异常,缓存无法正常存储,相当于失效。

  3. 执行 insert、update、delete 操作后失效:任何一个 SqlSession 执行了当前 Mapper 的 insert、update、delete 操作(无论是否提交),MyBatis 会自动清空该 Mapper 的二级缓存(确保缓存与数据库一致)。

  4. 查询标签配置 useCache="false":在单个查询标签中配置 useCache="false",表示该查询不使用二级缓存,每次查询都会发送 SQL,缓存失效。

  5. 缓存过期或达到缓存上限:二级缓存默认使用 LRU(最近最少使用)淘汰策略,当缓存数据达到配置的 size(如 size="1024"),会淘汰最少使用的数据;若配置了 flushInterval(过期时间),过期后缓存自动失效。

  6. 手动清空二级缓存:调用 sqlSessionFactory.getConfiguration().getCache(key).clear() 方法,可手动清空指定 Mapper 的二级缓存;或在 Mapper 映射文件中配置 <cache-ref> 导致缓存共享冲突,也可能引发失效。

(2).实战避坑

1. 避免在频繁修改的数据上使用二级缓存(如订单表、库存表),否则会导致缓存频繁失效,不仅无法提升性能,还会增加缓存维护成本;

2. 一级缓存失效的常见误区:认为同一 Mapper 方法、同一参数,不同 SqlSession 会命中一级缓存,实则一级缓存仅在单个 SqlSession 内有效;

3. 二级缓存失效排查:先检查缓存配置(cacheEnabled、<cache/>),再检查实体类是否序列化,最后排查是否有 insert/update/delete 操作触发缓存清空。

五、MyBatis 延迟加载原理

(1).面试标准回答

MyBatis 延迟加载(懒加载)是一种「按需加载数据」的机制,核心原理是“只有当程序需要使用关联对象的数据时,才发送 SQL 语句查询关联数据,而非查询主对象时就一次性加载所有关联数据”,目的是减少不必要的数据库访问,提升系统性能,底层基于动态代理和 ResultMap 配置实现。

MyBatis 延迟加载仅支持「关联查询」(一对一、一对多、多对多),默认不开启,需手动配置开启。

(一)、延迟加载的开启方式

需在 MyBatis 核心配置文件(mybatis-config.xml)中配置两个参数,开启全局延迟加载:

<setting>
  <!-- 开启全局延迟加载,默认false(立即加载) -->
  <setting name="lazyLoadingEnabled" value="true"/&gt;
  <!-- 关闭“积极加载”,即按需加载,默认true(积极加载所有关联对象) -->
  <setting name="aggressiveLazyLoading" value="false"/>
</settings>

补充:也可在单个关联标签中配置 lazyLoadTriggerMethods,指定触发延迟加载的方法(默认是 equals、clone、hashCode、toString),避免误触发加载。

(二)、延迟加载核心原理(源码级)

MyBatis 延迟加载的核心是「动态代理」,结合 ResultMap 中的关联配置,具体流程如下:

  1. 开启延迟加载后,MyBatis 执行主查询(如查询 User)时,会通过 JDK 动态代理生成主对象(User)的代理对象,而非直接返回真实的 User 对象;

  2. 主查询仅加载主对象的基本属性(如 User 的 id、name),不加载关联对象(如 User 关联的 Order 列表),此时关联对象为 null(或空代理集合);

  3. 当程序首次访问主对象的关联属性(如 user.getOrders())时,代理对象会拦截该方法,判断关联对象是否已加载;

  4. 若未加载,代理对象会通过 SqlSession 发送关联查询的 SQL(如查询该 User 的所有 Order),加载关联数据,初始化关联对象;

  5. 若已加载,直接返回关联对象,不再发送 SQL;

  6. 若 SqlSession 已关闭,代理对象无法获取 SqlSession 发送 SQL,会抛出 LazyInitializationException(懒加载异常)。

核心源码类:org.apache.ibatis.executor.loader.ResultLoader(负责延迟加载数据)、org.apache.ibatis.executor.loader.ProxyFactory(生成动态代理对象)。

(三)、延迟加载的适用场景及注意事项

  • 适用场景:关联对象数据不常使用、关联数据量较大的场景(如查询 User 时,很少需要访问其关联的 Order 列表),可减少主查询的 SQL 开销。

  • 注意事项:

    • 懒加载异常(LazyInitializationException):SqlSession 关闭后,访问关联对象会抛出该异常,解决方案:① 延长 SqlSession 生命周期(如 Web 项目中使用 OpenSessionInViewFilter);② 关闭延迟加载(针对该关联);③ 提前加载关联对象(如使用 fetchType="eager");

    • 延迟加载仅支持关联查询,单个对象查询(如 getById)不支持延迟加载;

    • 若关联对象必用,建议关闭延迟加载(fetchType="eager"),避免频繁触发关联查询,反而降低性能。

六、MyBatis 插件机制(Interceptor)

(1).面试标准回答

MyBatis 插件机制(Interceptor)是 MyBatis 核心扩展性机制,允许开发者在不修改 MyBatis 源码的前提下,通过自定义插件,拦截 MyBatis 执行流程中的核心方法,实现功能增强(如分页、日志记录、参数加密、SQL 改写)。

MyBatis 插件本质是「AOP 动态代理」,底层通过拦截器链(InterceptorChain)实现,仅能拦截 MyBatis 预设的 4 个核心接口的方法,无法拦截自定义方法。

(一)、插件可拦截的核心接口及方法(源码级)

MyBatis 仅允许拦截以下 4 个核心接口的方法,这是插件机制的核心限制,面试必答:

  1. Executor:MyBatis 核心执行器,负责 SQL 执行的整个流程,可拦截的核心方法:query(查询)、update(新增/修改/删除)、commit(提交)、rollback(回滚);

  2. StatementHandler:负责处理 Statement(PreparedStatement、Statement),可拦截的核心方法:prepare(创建 Statement)、parameterize(设置参数)、execute(执行 SQL);

  3. ParameterHandler:负责处理 SQL 参数,可拦截的核心方法:setParameters(给 PreparedStatement 设置参数);

  4. ResultSetHandler:负责处理查询结果集,可拦截的核心方法:handleResultSets(将结果集封装为 Java 对象)。

(二)、自定义插件的实现步骤(实战示例)

自定义 MyBatis 插件,需遵循 3 个步骤:实现 Interceptor 接口、添加 @Intercepts 注解、配置插件到 MyBatis 核心配置,以下是日志记录插件示例:

  1. 第一步:实现 Interceptor 接口,重写 3 个核心方法: 

     import org.apache.ibatis.plugin.*;
    import java.util.Properties;
    
    // @Intercepts:指定拦截的接口、方法、参数(核心注解)
    @Intercepts({
        @Signature(
            type = StatementHandler.class, // 拦截的接口
            method = "prepare", // 拦截的方法
            args = {java.sql.Connection.class, Integer.class} // 拦截方法的参数类型
        )
    })
    public class LogInterceptor implements Interceptor {
    
        // 核心方法:拦截目标方法,实现增强逻辑
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            // 1. 获取目标对象(StatementHandler)
            StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
            // 2. 增强逻辑:记录SQL语句
            String sql = statementHandler.getBoundSql().getSql();
            System.out.println("MyBatis 执行SQL:" + sql);
            // 3. 执行目标方法(放行,继续执行原逻辑)
            return invocation.proceed();
        }
    
        // 方法:生成目标对象的代理对象(MyBatis 内部调用,无需手动实现)
        @Override
        public Object plugin(Object target) {
            // 调用 MyBatis 提供的 Plugin 工具类,生成代理对象
            return Plugin.wrap(target, this);
        }
    
        // 方法:读取插件配置的属性(如在mybatis-config.xml中配置的参数)
        @Override
        public void setProperties(Properties properties) {
            // 示例:读取配置的参数(如日志级别)
            String logLevel = properties.getProperty("logLevel");
            System.out.println("插件日志级别:" + logLevel);
        }
    }

  2. 第二步:在 MyBatis 核心配置文件(mybatis-config.xml)中配置插件:

    <plugins>
      &lt;plugin interceptor="com.xxx.plugin.LogInterceptor"&gt;
        <!-- 可选:配置插件属性,通过setProperties方法读取 -->
        <property name="logLevel" value="INFO"/>
      </plugin>
    </plugins>

  3. 第三步:测试插件:执行 Mapper 方法,会自动拦截 StatementHandler 的 prepare 方法,打印执行的 SQL 语句,实现日志记录功能。

(三)、插件执行原理(源码级)

  1. MyBatis 初始化时,会读取核心配置文件中的 <plugins> 标签,将所有自定义插件注册到 InterceptorChain(拦截器链)中;

  2. 当 MyBatis 执行 SQL 时,会为可拦截的核心接口(如 StatementHandler)创建动态代理对象(通过 Plugin.wrap() 方法);

  3. 当调用目标方法(如 prepare())时,会先触发代理对象的 intercept() 方法,执行自定义增强逻辑;

  4. 增强逻辑执行完成后,通过 invocation.proceed() 方法,放行到下一个拦截器(若有),最终执行目标方法的原逻辑;

  5. 多个插件会按配置顺序组成拦截器链,依次执行每个插件的增强逻辑。

(2).面试补充(高频追问)

1. 插件的优先级:配置在 <plugins> 标签中前面的插件,先执行(拦截器链按配置顺序执行);

2. 常见插件应用:分页插件(PageHelper,拦截 Executor 的 query 方法,改写 SQL 实现分页)、数据加密插件(拦截 ParameterHandler 的 setParameters 方法,加密参数)、日志插件(记录 SQL 执行日志);

3. 插件开发注意事项:① 仅能拦截 MyBatis 预设的 4 个接口,不可拦截自定义 Mapper 方法;② 增强逻辑不宜过于复杂,避免影响 SQL 执行性能;③ 注意插件的兼容性,不同 MyBatis 版本的接口方法可能有差异。

七、MyBatis 执行流程(SqlSession -> Executor -> StatementHandler)

(1).面试标准回答

MyBatis 执行 SQL 的核心流程,围绕「SqlSession → Executor → StatementHandler → Statement → ResultSetHandler」展开,从获取 SqlSession 到返回查询结果,共 7 个核心步骤,每个步骤对应 MyBatis 核心组件的作用,源码级解析如下:

核心执行流程(7步,结合源码)

  1. 步骤1:获取 SqlSessionFactory通过 MyBatis 核心配置文件(mybatis-config.xml),构建 SqlSessionFactory 对象,这是 MyBatis 的核心工厂类,负责创建 SqlSession。

    // 构建 SqlSessionFactory
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

     源码细节:SqlSessionFactoryBuilder 会解析配置文件,生成 Configuration 对象(MyBatis 核心配置),再创建 DefaultSqlSessionFactory(SqlSessionFactory 的默认实现)。

  2. 步骤2:获取 SqlSession通过 SqlSessionFactory 的 openSession() 方法,获取 SqlSession 对象,SqlSession 是 MyBatis 与数据库交互的核心会话对象,封装了数据库连接和执行 SQL 的方法。

    // 获取 SqlSession(true表示自动提交事务,默认false)
    SqlSession session = sqlSessionFactory.openSession(true);

     源码细节:openSession() 方法会创建 Executor(执行器),再创建 DefaultSqlSession(SqlSession 的默认实现),将 Executor 注入 SqlSession。

  3. 步骤3:获取 Mapper 代理对象通过 SqlSession 的 getMapper() 方法,获取 Mapper 接口的动态代理对象(MyBatis 不要求 Mapper 有实现类,代理对象负责执行 SQL 逻辑)。

    UserMapper userMapper = session.getMapper(UserMapper.class);

     源码细节:getMapper() 方法通过 MapperProxyFactory 生成 Mapper 代理对象(JDK 动态代理),代理对象会将方法调用转发给 Executor。

  4. 步骤4:Executor 执行 SQL 调度调用 Mapper 代理对象的方法(如 userMapper.selectById(1)),代理对象会将方法参数、SQL 信息(从 Mapper 配置中获取)传递给 Executor,Executor 是 MyBatis 的核心执行器,负责 SQL 执行的调度。源码细节:Executor 有三种实现:SimpleExecutor(默认,每次执行 SQL 都创建新的 Statement)、ReuseExecutor(复用 Statement)、BatchExecutor(批量执行 SQL),可通过配置指定。

  5. 步骤5:StatementHandler 处理 StatementExecutor 会创建 StatementHandler 对象,由 StatementHandler 负责创建 Statement(PreparedStatement 或 Statement)、设置 SQL 参数、执行 SQL。核心流程:① StatementHandler 调用 prepare() 方法,通过 Connection 创建 PreparedStatement;② 调用 ParameterHandler 的 setParameters() 方法,给 PreparedStatement 设置参数;③ 调用 execute() 方法,执行 SQL,获取 ResultSet。

  6. 步骤6:ResultSetHandler 处理结果集   SQL 执行完成后,StatementHandler 会获取 ResultSet,交给 ResultSetHandler 处理,ResultSetHandler 会将 ResultSet 封装为 Java 对象(根据 Mapper 配置的 resultType 或 resultMap)。源码细节:ResultSetHandler 会根据配置的映射关系,将结果集中的字段值映射到 Java 对象的属性中,支持一对一、一对多关联映射。

  7. 步骤7:关闭 SqlSessionSQL 执行完成后,关闭 SqlSession,释放数据库连接(若未配置连接池,会直接关闭连接;若配置连接池,会将连接归还到连接池)。session.close();

核心组件关系总结

SqlSession(会话)→ Executor(执行器,调度核心)→ StatementHandler(处理 Statement,执行 SQL)→ ParameterHandler(设置参数)/ ResultSetHandler(处理结果集),各组件分工明确,协同完成 SQL 执行流程。

(2).面试补充

1. 执行流程中的缓存:Executor 会先查询一级缓存,缓存未命中再执行后续 SQL 流程,执行完成后将结果存入一级缓存;

2. 插件的介入:在 Executor、StatementHandler 等组件创建时,会通过插件机制生成代理对象,拦截对应的方法,实现功能增强;

3. 事务管理:SqlSession 中的事务由 Executor 管理,commit()、rollback() 方法会转发给 Executor,由 Executor 处理事务的提交和回滚。

八、ResultType 和 ResultMap 区别

(1).面试标准回答

ResultType 和 ResultMap 是 MyBatis 中两种核心的「结果集映射方式」,均用于将 SQL 执行后的 ResultSet 封装为 Java 对象,但适用场景、灵活性、配置方式有显著区别,面试常考两者的区别及选型,具体如下:

对比维度

ResultType

ResultMap

核心作用

指定结果集的封装类型(Java 实体类、基本类型、Map 等),要求 SQL 查询的字段名与 Java 对象的属性名完全一致(大小写不敏感,MyBatis 可配置驼峰映射)。

自定义结果集映射规则,解决「字段名与属性名不一致」问题,支持复杂关联映射(一对一、一对多、多对多),灵活性更高。

字段名与属性名映射

要求字段名与属性名一致(或通过驼峰映射配置,如数据库字段 user_name 对应 Java 属性 userName),无法自定义映射。

可自定义映射规则(通过 <result> 标签),如数据库字段 user_id 对应 Java 属性 id,无需字段名与属性名一致。

关联映射支持

不支持关联映射(一对一、一对多等),仅能封装单个实体类或简单类型。

支持复杂关联映射,通过 <association>(一对一)、<collection>(一对多)标签,实现关联对象的封装。

配置方式

直接在查询标签中配置 resultType 属性,无需额外配置(如 <select resultType="com.xxx.pojo.User">)。

需先在 Mapper 映射文件中定义 <resultMap> 标签,配置映射规则,再在查询标签中通过 resultMap 属性引用(如 <select resultMap="userResultMap">)。

适用场景

简单查询,字段名与 Java 对象属性名一致(或可通过驼峰映射匹配),无需关联映射(如查询单个实体、简单统计查询)。

复杂查询,字段名与属性名不一致,或需要关联映射(如一对一、一对多查询),需自定义映射规则的场景。

示例

<!-- 字段名与属性名一致,使用resultType -->
<select id="selectById" resultType="com.xxx.pojo.User">
  select id, name, age from user where id = #{id}
</select>

<!-- 定义resultMap,解决字段名与属性名不一致 -->
<resultMap id="userResultMap" type="com.xxx.pojo.User">
  <id column="user_id" property="id"/> <!-- 数据库字段user_id对应属性id -->
  <result column="user_name" property="name"/&gt; <!-- 数据库字段user_name对应属性name -->
  <result column="user_age" property="age"/>
</resultMap>

<!-- 引用resultMap -->
<select id="selectById" resultMap="userResultMap">
  select user_id, user_name, user_age from user where id = #{id}
</select>

(2).面试补充(高频追问)

1. 驼峰映射与 ResultType 的关系:MyBatis 可通过配置 <setting name="mapUnderscoreToCamelCase" value="true"/>,实现数据库字段(下划线命名,如 user_name)与 Java 属性(驼峰命名,如 userName)的自动映射,此时可使用 ResultType,无需手动配置 ResultMap;

2. ResultMap 的继承:可通过 <resultMap extends="父ResultMapId"> 实现 ResultMap 的继承,复用父 ResultMap 的映射规则,减少重复配置;

3. 选型建议:优先使用 ResultType(配置简单、效率高),当字段名与属性名不一致,或需要关联映射时,使用 ResultMap。

九、MyBatis 如何处理多表关联查询?

(1).面试标准回答

MyBatis 处理多表关联查询,核心是通过「ResultMap 自定义映射规则」,结合 <association>(一对一)、<collection>(一对多)、<association + 嵌套查询> 等方式,将多表查询的 ResultSet 封装为包含关联对象的 Java 实体类,支持一对一、一对多、多对多三种关联关系,具体实现方式如下(结合实战示例):

1. 一对一关联查询(如 User ↔ UserCard,一个用户对应一张身份证)

核心:使用 <association> 标签,在 ResultMap 中配置一对一关联,将关联表的字段映射到主实体类的关联属性中,分为「迫切连接(一次性加载)」和「嵌套查询(懒加载)」两种方式。

方式1:迫切连接(推荐,一次性加载主表+关联表数据)
<!-- 1. 定义ResultMap,配置一对一关联 -->
<resultMap id="userWithCardResultMap" type="com.xxx.pojo.User">
  <id column="user_id" property="id"/>
  <result column="user_name" property="name"/>
  <!-- association:一对一关联,property是主实体的关联属性名,javaType是关联对象类型 -->
  <association property="userCard" javaType="com.xxx.pojo.UserCard">
    <id column="card_id" property="id"/>
    <result column="card_no" property="cardNo"/>
    <result column="create_time" property="createTime"/>
  </association>
</resultMap>

<!-- 2. 多表连接查询,一次性加载所有数据 -->
<select id="selectUserWithCard" resultMap="userWithCardResultMap">
  select u.user_id, u.user_name, c.card_id, c.card_no, c.create_time
  from t_user u
  left join t_user_card c on u.id = c.user_id
  where u.id = #{id}
</select>

方式2:嵌套查询(懒加载,主表查询后,按需加载关联表数据)
<!-- 1. 主表ResultMap,配置嵌套查询 -->
<resultMap id="userResultMap" type="com.xxx.pojo.User">
  <id column="id" property="id"/&gt;
  &lt;result column="name" property="name"/&gt;
  <!-- select:关联查询的Mapper方法,column:主表与关联表的关联字段 -->
  <association 
    property="userCard" 
    javaType="com.xxx.pojo.UserCard"
    select="com.xxx.mapper.UserCardMapper.selectByUserId"
    column="id"
    fetchType="lazy"&gt; <!-- fetchType="lazy":开启懒加载 -->
  </association>
</resultMap>

<!-- 2. 主表查询 -->
<select id="selectUserById" resultMap="userResultMap">
  select id, name from t_user where id = #{id}
</select>

<!-- 3. 关联表查询(被嵌套调用) -->
<select id="selectByUserId" resultType="com.xxx.pojo.UserCard">
  select id, card_no as cardNo, create_time as createTime from t_user_card where user_id = #{userId}
</select>

2. 一对多关联查询(如 User ↔ Order,一个用户对应多个订单)

核心:使用 <collection> 标签,在 ResultMap 中配置一对多关联,将关联表的多条数据映射到主实体类的集合属性中(如 List<Order> orders),同样支持迫切连接和嵌套查询。

实战示例(迫切连接)
<!-- 1. 定义ResultMap,配置一对多关联 -->
<resultMap id="userWithOrdersResultMap" type="com.xxx.pojo.User">
  <id column="user_id" property="id"/>
  &lt;result column="user_name" property="name"/&gt;
  <!-- collection:一对多关联,property是集合属性名,ofType是集合中元素的类型 -->
  <collection property="orders" ofType="com.xxx.pojo.Order">
    <id column="order_id" property="id"/>
    <result column="order_no" property="orderNo"/>
    <result column="create_time" property="createTime"/>
  </collection>
</resultMap>

<!-- 2. 多表连接查询 -->
<select id="selectUserWithOrders" resultMap="userWithOrdersResultMap">
  select u.user_id, u.user_name, o.order_id, o.order_no, o.create_time
  from t_user u
  left join t_order o on u.id = o.user_id
  where u.id = #{id}
</select>

3. 多对多关联查询(如 Student ↔ Course,多个学生对应多个课程)

核心:多对多关联需要一张中间表(如 t_student_course),存储两个表的主键关联关系;MyBatis 处理时,将多对多拆分为两个一对多关联,通过 <collection> 标签配置,同样支持迫切连接和嵌套查询。

实战示例(迫切连接)

核心前提:多对多关联必须依赖中间表,先明确3张核心表结构(实战落地必备,面试可补充):

  • t_student(学生表):id(INT,主键)、student_name(VARCHAR,学生姓名)、student_age(INT,学生年龄)

  • t_course(课程表):id(INT,主键)、course_name(VARCHAR,课程名称)、course_credit(INT,学分)、course_desc(VARCHAR,课程描述)

  • t_student_course(中间表):id(INT,可选自增主键)、student_id(INT,外键,关联t_student.id)、course_id(INT,外键,关联t_course.id)

补充:实体类定义(简化版,贴合实战,面试可直接提及):

XML映射文件配置(迫切连接,一次性加载学生及关联的所有课程,核心实战代码):

补充:Mapper接口定义(实战必备,面试可完整提及):

测试代码(简化版,贴合实战,面试可简述流程):

迫切连接核心说明(面试加分点):

1. 核心逻辑:多对多关联通过“主表→中间表→关联表”的左连接,一次性加载所有数据,仅发送1次SQL,效率高于嵌套查询;

2. 关键注意:ResultMap中collection标签的ofType必须指定为关联实体类(Course),而非中间表;表连接时必须通过中间表的外键关联两张主表;

3. 优势:无需多次发送SQL,减少数据库交互,适合关联数据量不大、需一次性加载所有关联信息的场景(如学生详情页展示所选课程)。

面试补充(高频追问)

1. 多表关联查询的性能优化:优先使用迫切连接(一次性加载),减少数据库交互;数据量较大时,结合分页插件(如 PageHelper),避免一次性加载过多数据;

2. 关联查询的字段冲突:多表查询时,需给表添加别名(如 s、sc、c),明确区分同名字段(如 id),避免映射错误;

3. 懒加载异常:嵌套查询开启懒加载后,若 SqlSession 关闭后访问关联对象,会抛出 LazyInitializationException,解决方案:延长 SqlSession 生命周期,或关闭该关联的懒加载。

十、MyBatis 分页实现方式,RowBounds vs PageHelper

(1).面试标准回答

MyBatis 本身未提供内置的分页功能,需通过手动实现或第三方插件实现分页,常用的两种方式是「RowBounds 分页」和「PageHelper 分页」,两者在实现原理、使用方式、性能上有显著区别,具体对比及实战示例如下:

(一)、RowBounds 分页(MyBatis 内置,基础分页方式)

RowBounds 是 MyBatis 内置的分页工具类,无需导入额外依赖,核心原理是「内存分页」—— 先查询出所有符合条件的数据,再在内存中截取指定范围的记录,本质是“假分页”。

1. 核心使用方式

2. 核心原理

RowBounds 通过拦截器(RowBoundsInterceptor),在 ResultSetHandler 处理结果集时,跳过 offset 条记录,只读取 limit 条记录,本质是“先查全量数据,再内存截取”,未减少数据库的查询压力。

3. 优缺点

  • 优点:无需导入额外依赖,MyBatis 内置,配置简单,适合少量数据分页;

  • 缺点:内存分页,大数据量场景下(如万级以上数据),会查询大量冗余数据,占用内存,性能极差,不适合生产环境。

(二)、PageHelper 分页(第三方插件,推荐生产使用)

PageHelper 是 MyBatis 最常用的分页插件,基于 MyBatis 插件机制实现,核心原理是「数据库分页」—— 自动拦截查询 SQL,在 SQL 末尾添加分页语句(如 MySQL 的 limit、Oracle 的 rownum),让数据库只返回分页范围内的记录,本质是“真分页”,性能优于 RowBounds。

1. 核心使用步骤(实战落地)

  1. 第一步:导入依赖(Maven) <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.3.2</version> </dependency>

  2. 第二步:配置 PageHelper 插件(MyBatis 核心配置文件) <plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <!-- 配置数据库方言,告诉PageHelper使用哪种数据库的分页语法 --> <property name="helperDialect" value="mysql"/> <!-- 开启合理化分页(避免页码小于1时查询第1页,大于最大页码时查询最后一页) --> <property name="reasonable" value="true"/> </plugin> </plugins>

  3. 第三步:代码中使用(两种常用方式) // 方式1:使用PageHelper.startPage()静态方法(推荐) SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); // 分页参数:第1页,每页5条 PageHelper.startPage(1, 5); // 后续的第一个查询方法,会自动被分页 List<User> userList = mapper.selectAll(); // 封装分页结果(获取总条数、总页数等信息) PageInfo<User> pageInfo = new PageInfo<User>(userList); System.out.println("总条数:" + pageInfo.getTotal()); System.out.println("总页数:" + pageInfo.getPages()); // 方式2:使用Page对象作为返回值 Page<User> selectUserByPage(@Param("age") Integer age); // 调用时自动分页 Page<User> page = mapper.selectUserByPage(18); System.out.println("当前页数据:" + page.getResult()); System.out.println("总条数:" + page.getTotal());

2. 核心原理

PageHelper 实现了 MyBatis 的 Interceptor 接口,拦截 Executor 的 query 方法,在执行 SQL 之前,根据配置的数据库方言,自动给 SQL 末尾添加分页语句(如 MySQL 会添加 “limit offset, limit”),让数据库只查询分页范围内的数据,减少数据传输和内存占用。

(三)、RowBounds vs PageHelper 核心对比

对比维度

RowBounds

PageHelper

分页类型

内存分页(假分页)

数据库分页(真分页)

实现原理

先查询全量数据,再在内存中截取指定范围记录

拦截 SQL,自动添加分页语句,数据库只返回分页数据

性能

大数据量下性能极差,占用内存多

性能优秀,减少数据库查询和数据传输压力

依赖

MyBatis 内置,无需额外依赖

需导入 PageHelper 依赖,配置插件

功能

仅支持基础分页,无总条数、总页数等信息

支持分页、排序、合理化分页,可获取总条数、总页数等详情

适用场景

少量数据分页、测试场景

生产环境、大数据量分页(推荐)

(2).面试补充(高频追问)

1. PageHelper 分页的注意事项:PageHelper.startPage() 方法需在查询方法之前调用,且只对后续的第一个查询方法生效;

2. 数据库方言配置:若不配置 helperDialect,PageHelper 会自动检测数据库方言,但建议手动配置,避免检测错误;

3. 分页插件的扩展:除了 PageHelper,还有 MyBatis-Plus 内置的分页插件(PaginationInnerInterceptor),用法与 PageHelper 类似,更适合 MyBatis-Plus 项目。

十一、MyBatis 绑定接口 Mapper 原理(JDK 动态代理)

(1).面试标准回答

MyBatis 中,Mapper 接口(如 UserMapper)无需编写实现类,就能被程序调用并执行对应的 SQL 语句,核心原理是「JDK 动态代理」—— MyBatis 会为每个 Mapper 接口生成动态代理对象,代理对象将方法调用转发给 MyBatis 核心组件(Executor、StatementHandler 等),最终执行 SQL 并返回结果,具体原理和流程如下:

(一). 核心前提

Mapper 接口必须满足两个条件,才能被 MyBatis 识别并生成代理对象:

  • Mapper 接口的全路径(如 com.xxx.mapper.UserMapper),必须与对应的 Mapper XML 映射文件的 namespace 属性一致;

  • Mapper 接口中的方法名,必须与 Mapper XML 中 <select>、<insert> 等标签的 id 属性一致,方法参数、返回值需与 SQL 语句的参数、结果集类型匹配。

(二). 动态代理核心原理(源码级流程)

MyBatis 生成 Mapper 代理对象的核心类是 MapperProxyFactory,流程分为 4 步,本质是 JDK 动态代理的应用(JDK 动态代理基于接口,不依赖实现类,贴合 Mapper 接口无实现类的场景):

  1. 步骤1:初始化加载 Mapper 接口MyBatis 初始化时,会解析 Mapper 接口和对应的 XML 映射文件,将 Mapper 接口的信息(方法名、参数、返回值)封装为 MappedStatement 对象,存入 Configuration 配置中(MyBatis 核心配置类)。

  2. 步骤2:获取 Mapper 代理对象当程序调用 sqlSession.getMapper(Mapper.class) 方法时,MyBatis 会通过 MapperProxyFactory,创建 Mapper 接口的动态代理对象(Proxy 实例),代理对象的 InvocationHandler 是 MapperProxy(MyBatis 自定义的调用处理器)。// 源码核心逻辑(简化) public <T> T getMapper(Class<T> type, SqlSession sqlSession) { // 获取 MapperProxyFactory MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } // 生成动态代理对象,传入 MapperProxy(调用处理器) return mapperProxyFactory.newInstance(sqlSession); }

  3. 步骤3:代理对象拦截方法调用当程序调用 Mapper 代理对象的方法(如 userMapper.selectById(1))时,会被 MapperProxy 的 invoke() 方法拦截(JDK 动态代理的核心,所有方法调用都会转发到 invoke 方法)。

  4. 步骤4:转发方法调用,执行 SQLMapperProxy 的 invoke() 方法会做以下操作,最终执行 SQL:// MapperProxy 的 invoke 方法核心逻辑(简化) @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 处理 Object 类的方法(如 toString、hashCode),直接执行 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } // 处理 Mapper 接口的方法,转发给 SqlSession 执行 return mapperMethod.execute(sqlSession, args); }

    1. 解析当前调用的方法(方法名、参数),从 Configuration 中获取对应的 MappedStatement(包含 SQL 语句、参数类型、结果类型等信息);

    2. 将方法调用转发给 SqlSession 的对应方法(如 selectOne、selectList),由 SqlSession 调用 Executor、StatementHandler 等组件,执行 SQL 并封装结果;

    3. 将 SQL 执行结果返回给调用者。

(三). 核心总结

MyBatis Mapper 接口绑定的本质是「JDK 动态代理」:

  • 代理对象:MyBatis 为 Mapper 接口生成 Proxy 代理实例,无真实实现类;

  • 调用处理器:MapperProxy,负责拦截 Mapper 方法调用,转发给 MyBatis 核心组件;

  • 关键匹配:Mapper 接口与 XML 的 namespace、方法名与标签 id 必须一致,否则无法找到对应的 SQL 语句,抛出 BindingException 异常。

(2).面试补充(高频追问)

1. 为什么 MyBatis 用 JDK 动态代理,而非 CGLIB 动态代理?

答:因为 Mapper 是接口,JDK 动态代理本身就是基于接口实现的,无需依赖实现类,且轻量级、效率高;CGLIB 是基于类的动态代理,需要生成子类,而 Mapper 接口无实现类,无法使用 CGLIB(MyBatis 也支持 CGLIB 代理,可通过配置修改,但默认是 JDK 动态代理)。

2. Mapper 接口中可以定义重载方法吗?

答:不建议定义,MyBatis 是通过「方法名」匹配 XML 中的 SQL 语句,重载方法名相同,会导致 MyBatis 无法区分对应的 MappedStatement,抛出绑定异常;若需实现类似重载的功能,可通过 @Param 注解区分参数,或修改方法名。

3. 注解方式的 Mapper(如 @Select),绑定原理和 XML 方式一致吗?

答:一致,都是通过 JDK 动态代理生成代理对象,区别仅在于 SQL 语句的配置方式(注解直接配置在方法上,XML 配置在文件中),解析时都会封装为 MappedStatement,后续执行流程完全相同。

十二、MyBatis 事务管理方式

(1).面试标准回答

MyBatis 本身提供了事务管理机制,核心是「事务的提交、回滚、关闭」,底层依赖 JDBC 事务或 Spring 事务管理,支持两种事务管理方式:「JDBC 事务管理」和「Spring 事务管理」,其中 Spring 事务管理是企业开发中的主流方式,具体说明如下:

1. 核心前提(事务的四大特性)

MyBatis 事务遵循 ACID 特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),事务管理的核心目的是保证数据操作的一致性和完整性。

2. 方式一:JDBC 事务管理(MyBatis 原生,独立使用)

MyBatis 原生的事务管理,底层依赖 JDBC 的事务机制(java.sql.Connection),由 SqlSession 直接管理事务,核心是通过 Connection 的 commit()、rollback() 方法实现事务的提交和回滚,默认关闭自动提交。

核心使用方式(实战示例)
核心特点
  • 底层依赖 JDBC Connection,事务的边界由 SqlSession 控制(一个 SqlSession 对应一个 Connection,一个事务);

  • 默认不自动提交(autoCommit=false),需手动调用 commit() 提交事务,发生异常时调用 rollback() 回滚;

  • 局限性:仅能在单个 SqlSession 内管理事务,无法实现跨 SqlSession 的事务管理,适合 MyBatis 独立使用(不整合 Spring)的简单场景。

3. 方式二:Spring 事务管理(主流,企业开发首选)

当 MyBatis 与 Spring 整合时,通常使用 Spring 事务管理,Spring 提供了声明式事务(注解方式)和编程式事务两种方式,核心是「Spring 接管事务管理」,MyBatis 不再负责事务,而是由 Spring 统一管理 Connection,实现跨 SqlSession、跨 Mapper 的事务控制。

方式2.1:声明式事务(注解方式,最常用)

通过 @Transactional 注解,快速实现事务管理,无需手动编写 commit、rollback 代码,Spring 自动完成事务的提交和回滚。

方式2.2:编程式事务(手动控制,适合复杂场景)

通过 Spring 提供的 TransactionTemplate,手动控制事务的提交、回滚,适合需要灵活控制事务边界的场景。

核心特点
  • Spring 接管事务,通过 DataSourceTransactionManager 管理 Connection,实现跨 SqlSession 的事务控制(多个 SqlSession 可共享同一个 Connection);

  • 声明式事务(@Transactional)配置简单,无需手动控制事务,适合大多数场景;

  • 支持事务隔离级别、传播行为的配置(如 @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)),灵活适配复杂业务场景;

  • 核心优势:与 Spring 生态深度整合,支持分布式事务(结合 Spring Cloud Alibaba Seata 等组件)。

4. MyBatis 事务管理的核心注意事项
  • 事务的边界:事务应作用于业务层(Service),而非 Dao 层(Mapper),避免单个 Dao 方法单独提交事务,导致事务不一致;

  • 自动提交配置:MyBatis 原生 SqlSession 默认 autoCommit=false,Spring 事务管理中,默认会根据 @Transactional 注解自动控制提交和回滚;

  • 异常回滚:Spring 声明式事务中,默认只回滚 RuntimeException 及其子类,若需回滚所有异常,需配置 rollbackFor = Exception.class;

  • 事务隔离级别:MyBatis 支持 JDBC 的 4 种隔离级别(READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE),可通过配置或注解指定,默认隔离级别与数据库一致(MySQL 默认 REPEATABLE_READ)。

(2).面试补充(高频追问)

1. MyBatis 事务与 Spring 事务的关系?

答:MyBatis 原生事务是基于 JDBC 的简单事务管理,适合独立使用;当与 Spring 整合时,Spring 会接管事务管理,MyBatis 不再负责 Connection 的提交和回滚,而是由 Spring 的事务管理器统一控制,实现更灵活、更强大的事务功能。

2. 什么是事务传播行为?MyBatis 结合 Spring 时,常用的传播行为有哪些?

答:事务传播行为定义了多个事务方法嵌套调用时,事务的传播规则,常用的有:

  • REQUIRED(默认):如果当前有事务,就加入当前事务;如果没有事务,就创建一个新事务;

  • REQUIRES_NEW:无论当前是否有事务,都创建一个新事务,原有事务暂停;

  • SUPPORTS:如果当前有事务,就加入当前事务;如果没有事务,就以非事务方式执行。

3. MyBatis 事务中,SqlSession 与 Connection 的关系?

答:一个 SqlSession 对应一个 Connection,MyBatis 原生事务中,事务的边界与 SqlSession 一致(一个 SqlSession 内的所有操作,属于同一个事务);

Spring 事务管理中,Spring 会为同一个事务分配一个 Connection,多个 SqlSession 可共享该 Connection,确保事务一致性。

Logo

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

更多推荐