欢迎光临
我们一直在努力

MyBatis 一连串提问,被面试官吊打了!

引言:一场“血淋淋”的面试

“请介绍一下 MyBatis 中 #{} 和 ${} 的区别。”

面试官面无表情地抛出第一个问题,我心中一喜——这不是背烂了的八股文吗?于是流利地回答:“#{} 是预编译处理,可以防止 SQL 注入;${} 是字符串替换,不能防止 SQL 注入……”

“嗯,那 MyBatis 的一级缓存是 SqlSession 级别的,二级缓存是 Mapper 级别的,你能说说它们的工作流程吗?”

我继续作答,但逐渐感到面试官的问题越来越深入:“一级缓存什么时候会被清空?”“二级缓存如何实现分布式缓存?”“MyBatis 的插件原理是什么?如何分页?”

随着问题层层递进,我开始支支吾吾,最终被吊打得体无完肤……

如果你也曾经历过类似场景,或者想彻底掌握 MyBatis 的核心原理与面试要点,那么这篇 2万字详解 将是你的救命稻草。本文将以面试问答形式,从基础到源码,全面梳理 MyBatis 的知识体系。


1. MyBatis 基础概念

1.1 什么是 MyBatis?

面试官:请简单介绍一下 MyBatis 是什么,它和 Hibernate 有什么不同?

答:MyBatis 是一个半自动化的 ORM(对象关系映射)框架,它通过 XML 或注解的方式将 Java 对象与 SQL 语句映射起来。与 Hibernate 这种全自动 ORM 不同,MyBatis 需要开发者编写 SQL,因此灵活性更高,适合 SQL 优化复杂的场景。

核心特点:

  • 轻量级,学习成本低。

  • 支持动态 SQL,可以灵活组装查询条件。

  • 与 JDBC 相比,减少了大量冗余代码。

  • 支持与 Spring 等框架无缝集成。

与 Hibernate 对比:

特性MyBatisHibernate
SQL 控制 全手动编写 SQL 自动生成 SQL(也可手写)
学习曲线 平缓 较陡峭
数据库无关性 弱(SQL 依赖数据库) 强(HQL 屏蔽差异)
性能调优 容易(直接优化 SQL) 较复杂(需理解缓存、懒加载等)
适用场景 需细粒度 SQL 优化的项目 业务逻辑简单的 CRUD 项目

1.2 MyBatis 的核心组件

面试官:MyBatis 的核心组件有哪些?它们的作用分别是什么?

答:MyBatis 的核心组件包括:

  • SqlSessionFactoryBuilder:用于构建 SqlSessionFactory,一般从 XML 配置文件或 Configuration 对象构建,构建完成后即可丢弃。

  • SqlSessionFactory:工厂接口,用于创建 SqlSession。它是线程安全的,通常一个应用只有一个实例。

  • SqlSession:代表一次数据库会话,提供了执行 SQL、提交/回滚事务、获取 Mapper 接口等方法。它不是线程安全的,作用域通常限定在方法内。

  • Mapper:由 SqlSession 动态生成的代理对象,封装了 SQL 语句的调用细节。开发者通过 Mapper 接口调用方法,即可执行对应的 SQL。

工作流程:

  • 通过 SqlSessionFactoryBuilder 读取配置文件,创建 SqlSessionFactory。

  • 调用 sqlSessionFactory.openSession() 获取 SqlSession。

  • SqlSession 根据 Mapper 接口的字节码生成代理对象。

  • 调用 Mapper 方法,代理对象根据方法签名找到对应的 SQL 语句并执行。

  • 关闭 SqlSession(或由 Spring 管理)。


  • 2. MyBatis 配置文件详解

    MyBatis 的全局配置文件(通常命名为 mybatis-config.xml)包含了影响框架行为的设置。下面逐一讲解。

    2.1 properties 属性

    作用:用于引入外部 properties 文件或定义属性值,方便在配置文件中使用占位符。

    xml

    <properties resource="db.properties">
    <property name="username" value="root"/>
    </properties>

    之后在数据源配置中可引用:${driver}、${url} 等。属性加载顺序(后覆盖前):方法参数传递 > 文件 resource/url 属性 > 内部 property 子元素。

    2.2 settings 设置

    作用:全局参数配置,影响 MyBatis 运行时行为。例如:

    xml

    <settings>
    <!– 开启二级缓存 –>
    <setting name="cacheEnabled" value="true"/>
    <!– 开启延迟加载 –>
    <setting name="lazyLoadingEnabled" value="true"/>
    <!– aggressiveLazyLoading –>
    <setting name="aggressiveLazyLoading" value="false"/>
    <!– 使用驼峰命名自动转换 –>
    <setting name="mapUnderscoreToCamelCase" value="true"/>
    <!– 打印 SQL 日志 –>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>

    每个 setting 都有默认值,常用配置包括缓存、懒加载、执行器类型等。

    2.3 typeAliases 类型别名

    作用:为 Java 类型设置短名称,减少 XML 中全限定名的冗余。

    xml

    <typeAliases>
    <typeAlias alias="User" type="com.example.model.User"/>
    <!– 扫描包,默认别名是类名首字母小写 –>
    <package name="com.example.model"/>
    </typeAliases>

    内置别名如 _int、string、map、list 等。

    2.4 typeHandlers 类型处理器

    作用:实现 JDBC 类型与 Java 类型的相互转换。MyBatis 内置了常用类型处理器,如 String、Integer、Date 等。如果需要自定义(如处理 JSON 字段),可以实现 TypeHandler 接口或继承 BaseTypeHandler。

    xml

    <typeHandlers>
    <typeHandler handler="com.example.handler.JsonTypeHandler" javaType="com.example.model.JsonData" jdbcType="VARCHAR"/>
    </typeHandlers>

    2.5 plugins 插件

    作用:拦截 MyBatis 核心对象的方法调用,实现分页、性能监控等功能。后面单独章节详解。

    2.6 environments 环境配置

    作用:配置数据源和事务管理器,可以设置多个环境(开发、测试、生产),通过 default 指定。

    xml

    <environments default="development">
    <environment id="development">
    <transactionManager type="JDBC"/>
    <dataSource type="POOLED">
    <property name="driver" value="${driver}"/>
    <property name="url" value="${url}"/>
    <property name="username" value="${username}"/>
    <property name="password" value="${password}"/>
    </dataSource>
    </environment>
    </environments>

    事务管理器类型:JDBC(使用 Connection 的提交回滚)、MANAGED(容器管理事务)。数据源类型:UNPOOLED(每次打开新连接)、POOLED(连接池)、JNDI。

    2.7 mappers 映射器

    作用:注册 SQL 映射文件或 Mapper 接口。

    xml

    <mappers>
    <!– 使用类路径加载 XML –>
    <mapper resource="com/example/mapper/UserMapper.xml"/>
    <!– 使用 Mapper 接口(需同名同路径的 XML 或注解 SQL) –>
    <mapper class="com.example.mapper.UserMapper"/>
    <!– 扫描包(推荐) –>
    <package name="com.example.mapper"/>
    </mappers>


    3. 映射器 XML 文件详解

    映射器 XML 文件包含了 SQL 语句、结果映射、缓存配置等。一个典型的映射器文件结构如下:

    xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.example.mapper.UserMapper">
    <!– 缓存配置 –>
    <cache/>
    <!– 结果映射 –>
    <resultMap id="userMap" type="User">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    </resultMap>
    <!– SQL 片段 –>
    <sql id="baseColumns">id, name, age</sql>
    <!– 查询语句 –>
    <select id="selectUser" resultMap="userMap">
    SELECT <include refid="baseColumns"/> FROM user WHERE id = #{id}
    </select>
    </mapper>

    3.1 CRUD 操作

    • <insert>、<update>、<delete>、<select> 标签对应增删改查。

    • 常用属性:id(方法名)、parameterType(参数类型)、resultType/resultMap(返回值类型)、flushCache(是否清空一级/二级缓存)、useCache(是否使用二级缓存)等。

    示例:

    xml

    <insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO user(name, age) VALUES(#{name}, #{age})
    </insert>

    3.2 resultMap 高级结果映射

    resultMap 是 MyBatis 最强大的特性之一,用于解决复杂查询结果到 Java 对象的映射。

    基本属性:

    • id:唯一标识。

    • type:映射的 Java 类型。

    • extends:继承其他 resultMap。

    子元素:

    • <id>:标记主键,可提升性能。

    • <result>:普通字段映射。

    • <association>:一对一关联。

    • <collection>:一对多关联。

    • <discriminator>:鉴别器(根据字段值选择不同的映射)。

    关联映射示例:

    xml

    <!– 用户和订单的一对多关系 –>
    <resultMap id="userWithOrders" type="User">
    <id property="id" column="u_id"/>
    <result property="name" column="u_name"/>
    <collection property="orders" ofType="Order">
    <id property="id" column="o_id"/>
    <result property="orderNo" column="order_no"/>
    </collection>
    </resultMap>

    <select id="selectUserWithOrders" resultMap="userWithOrders">
    SELECT u.id u_id, u.name u_name, o.id o_id, o.order_no
    FROM user u
    LEFT JOIN order o ON u.id = o.user_id
    WHERE u.id = #{id}
    </select>

    延迟加载:在 MyBatis 中,可以通过配置 lazyLoadingEnabled=true 和 aggressiveLazyLoading=false 来启用延迟加载。同时,association 和 collection 可以设置 fetchType="lazy" 覆盖全局配置。

    xml

    <association property="address" column="address_id" select="selectAddress" fetchType="lazy"/>

    3.3 动态 SQL

    动态 SQL 是 MyBatis 的核心优势,可以在 XML 中编写类似 JSTL 的条件判断、循环等逻辑。

    常用标签:

    • <if>:条件判断。

    • <choose>、<when>、<otherwise>:多分支选择。

    • <where>、<set>、<trim>:智能处理 AND/OR 和逗号。

    • <foreach>:循环集合。

    • <bind>:创建变量(如模糊查询拼接)。

    示例:

    xml

    <select id="findActiveUsers" resultType="User">
    SELECT * FROM user
    <where>
    <if test="name != null and name != ''">
    AND name LIKE CONCAT('%', #{name}, '%')
    </if>
    <if test="age != null">
    AND age = #{age}
    </if>
    </where>
    </select>

    <update id="updateUser">
    UPDATE user
    <set>
    <if test="name != null">name = #{name},</if>
    <if test="age != null">age = #{age},</if>
    </set>
    WHERE id = #{id}
    </update>

    <select id="selectByIds" resultType="User">
    SELECT * FROM user WHERE id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
    #{id}
    </foreach>
    </select>

    <select id="searchUsers" resultType="User">
    <bind name="pattern" value="'%' + keyword + '%'"/>
    SELECT * FROM user WHERE name LIKE #{pattern}
    </select>


    4. 缓存机制

    4.1 一级缓存(Local Cache)

    面试官:请说明 MyBatis 一级缓存的工作原理,以及它可能导致的问题。

    答:一级缓存是 SqlSession 级别的缓存,默认开启且无法关闭(在 MyBatis 3 中可配置 localCacheScope 为 STATEMENT 来关闭,但实际是清空缓存)。它基于 PerpetualCache 实现,底层是一个 HashMap。

    工作流程:

  • 执行查询时,先从缓存中根据 CacheKey 查找结果。

  • 如果找到,直接返回缓存对象。

  • 如果未找到,查询数据库,然后将结果放入缓存。

  • CacheKey 的组成:

    • MappedStatement 的 id。

    • 查询参数的组合(包括分页参数等)。

    • 具体的 SQL 语句(包括动态 SQL 生成的 SQL)。

    • 分页的 RowBounds 等。

    一级缓存失效(清空)情况:

    • 执行 SqlSession 的 update、delete、insert 操作(即使影响行数为0),会清空一级缓存。

    • 手动调用 clearCache() 方法。

    • 执行 commit()、rollback() 或 close() 后,SqlSession 关闭,缓存自然消失。

    • 如果配置了 localCacheScope=STATEMENT,则每次查询后清空缓存(相当于缓存只在当前语句有效)。

    注意问题:由于一级缓存是 SqlSession 级别的,在 Spring 整合中,如果 Service 方法未开启事务,每次 Mapper 操作都会新建 SqlSession(或使用 SqlSessionTemplate 的线程绑定机制),一级缓存不共享,因此不会出现脏数据。但在事务方法内,多次查询可能使用同一 SqlSession,此时需注意缓存可能带来的数据不一致问题(如其他事务修改了数据,但当前 SqlSession 缓存未更新)。

    4.2 二级缓存(Second Level Cache)

    面试官:那二级缓存呢?它如何工作?有什么优缺点?

    答:二级缓存是 Mapper 级别(namespace)的缓存,可以被多个 SqlSession 共享。它基于装饰器模式,默认也是用 PerpetualCache,但可以配置如 Redis、EhCache 等第三方缓存实现。

    开启方式:

  • 在全局配置文件 <settings> 中设置 cacheEnabled=true(默认就是 true)。

  • 在 Mapper XML 中添加 <cache/> 标签,或使用 @CacheNamespace 注解。

  • 查询语句需要设置 useCache=true(默认 true)。

  • <cache/> 标签属性:

    • eviction:缓存回收策略(LRU、FIFO、SOFT、WEAK),默认 LRU。

    • flushInterval:刷新间隔(毫秒),默认不设置(仅当调用更新语句时刷新)。

    • size:引用数目,默认 1024。

    • readOnly:是否只读,true 则返回缓存对象本身(速度快但可能并发修改),false 则返回拷贝对象(安全但性能稍低),默认 false。

    • type:自定义缓存实现类。

    工作流程:

  • 二级缓存作用域为 namespace,所有 SqlSession 共享。

  • 当 SqlSession 执行查询时,先查二级缓存(若开启),再查一级缓存,最后查数据库。

  • 当执行更新操作(insert/update/delete)时,会清空该 namespace 下的二级缓存(包括关联的其他 namespace 如果配置了 <cache-ref>)。

  • 与一级缓存的关系:

    • 二级缓存的数据在 SqlSession 提交或关闭后才生效(因为事务未提交时数据可能回滚,不能放入二级缓存)。

    • 查询时数据加载顺序:二级缓存 → 一级缓存 → 数据库。

    脏数据问题:

    • 由于二级缓存是 namespace 级别的,当不同 namespace 操作关联表时可能导致数据不一致。例如,UserMapper 缓存了用户数据,但 OrderMapper 更新了用户订单(不涉及用户表),此时 User 缓存未失效,查询到旧数据。

    • 解决方案:使用 <cache-ref> 让多个 namespace 共享同一个缓存区域,或避免在关联查询中使用二级缓存。

    分布式缓存:
    可以通过实现 Cache 接口,将缓存数据存放到 Redis 等中间件中,实现分布式环境下的缓存共享。MyBatis 提供了 org.mybatis.caches.redis.RedisCache 等第三方集成包。

    4.3 自定义缓存

    面试官:如果需要集成 Redis 作为二级缓存,你会怎么做?

    答:可以自定义一个实现 org.apache.ibatis.cache.Cache 接口的类,并在 <cache type="…"/> 中指定。主要实现方法:getId、putObject、getObject、removeObject、clear、getSize 等。

    示例(伪代码):

    java

    public class RedisCache implements Cache {
    private final String id;
    private JedisPool jedisPool;

    public RedisCache(String id) {
    this.id = id;
    // 初始化连接池
    }

    @Override
    public void putObject(Object key, Object value) {
    // 序列化 key 和 value,存入 Redis
    }

    @Override
    public Object getObject(Object key) {
    // 从 Redis 获取并反序列化
    }

    // … 其他方法
    }

    然后在 Mapper 中配置:

    xml

    <cache type="com.example.RedisCache" eviction="LRU" flushInterval="60000"/>


    5. 插件开发

    5.1 插件原理

    面试官:MyBatis 的插件(Interceptor)是如何工作的?你能实现一个分页插件吗?

    答:MyBatis 允许拦截以下接口的方法:

    • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

    • ParameterHandler (getParameterObject, setParameters)

    • ResultSetHandler (handleResultSets, handleOutputParameters)

    • StatementHandler (prepare, parameterize, batch, update, query)

    插件通过动态代理实现,每个被拦截的对象都会被代理对象包装,执行时先调用插件的 intercept 方法。

    实现步骤:

  • 实现 Interceptor 接口。

  • 使用 @Intercepts 和 @Signature 注解指定要拦截的方法。

  • 在全局配置文件中注册插件。

  • 5.2 分页插件实现

    分页是常见需求,我们可以通过拦截 Executor 的 query 方法,在 SQL 后拼接 limit 语句。

    java

    @Intercepts({
    @Signature(type = Executor.class, method = "query",
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
    })
    public class PageInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
    Object[] args = invocation.getArgs();
    MappedStatement ms = (MappedStatement) args[0];
    Object parameter = args[1];
    RowBounds rowBounds = (RowBounds) args[2];

    // 判断是否需要分页(例如参数中带有 Page 对象)
    if (parameter instanceof Page) {
    Page page = (Page) parameter;
    // 获取 BoundSql
    BoundSql boundSql = ms.getBoundSql(parameter);
    String sql = boundSql.getSql();
    // 拼接 count 查询获取总记录数(省略)
    // 拼接分页 SQL
    String pageSql = sql + " LIMIT " + page.getOffset() + "," + page.getPageSize();
    // 通过反射修改 BoundSql 中的 SQL
    ReflectUtil.setFieldValue(boundSql, "sql", pageSql);
    }
    return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
    return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    // 可读取配置参数
    }
    }

    注意:实际实现更复杂,需要考虑方言(不同数据库分页语法)、预编译参数处理等。现在已有成熟分页插件如 PageHelper。


    6. 与 Spring 整合

    6.1 整合方式

    面试官:MyBatis 如何与 Spring 整合?整合后有什么好处?

    答:Spring 通过 mybatis-spring 桥接包提供无缝整合。主要配置:

    • SqlSessionFactoryBean:用于创建 MyBatis 的 SqlSessionFactory,替代原生 SqlSessionFactoryBuilder。

    • MapperFactoryBean 或 MapperScannerConfigurer:用于自动生成 Mapper 代理并注入 Spring 容器。

    • SqlSessionTemplate:替代原生的 SqlSession,保证线程安全,并与 Spring 事务同步。

    配置示例(JavaConfig 方式):

    java

    @Configuration
    @MapperScan("com.example.mapper")
    public class MyBatisConfig {
    @Bean
    public DataSource dataSource() {
    return new HikariDataSource();
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
    SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
    factoryBean.setDataSource(dataSource());
    factoryBean.setTypeAliasesPackage("com.example.model");
    factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
    .getResources("classpath:mapper/*.xml"));
    return factoryBean.getObject();
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplate() throws Exception {
    return new SqlSessionTemplate(sqlSessionFactory());
    }
    }

    整合好处:

    • Spring 管理数据源和事务,MyBatis 操作自动参与事务。

    • Mapper 接口可直接注入,无需手动获取 SqlSession。

    • SqlSessionTemplate 线程安全,避免原生 SqlSession 的线程不安全问题。

    6.2 事务管理

    面试官:在 Spring 整合中,MyBatis 是如何与 Spring 事务保持一致的?

    答:当使用 SqlSessionTemplate 时,它内部会通过 SpringManagedTransaction 管理事务。SqlSessionTemplate 的每个方法都会检查当前是否存在 Spring 事务(通过 TransactionSynchronizationManager),如果有,则复用同一个 SqlSession;如果没有,则创建新的 SqlSession,并在操作完成后自动提交/回滚。这样可以保证在同一个事务中,所有 Mapper 操作使用同一个数据库连接,保持事务一致性。


    7. 常见面试题深度解析

    7.1 #{} 和 ${} 的区别

    问题:请详细说明 #{} 和 ${} 的区别,并举例说明各自的使用场景。

    答:

    • #{}:预编译处理,MyBatis 会将 SQL 中的 #{} 替换为 ?,然后通过 PreparedStatement 的 set 方法赋值。这可以防止 SQL 注入,并且能够自动进行类型转换。

    • ${}:字符串替换,MyBatis 会将 SQL 中的 ${} 直接替换为参数值(原样拼接),不经过预编译。容易引发 SQL 注入,但适用于动态表名、列名等无法使用占位符的场景。

    示例:

    xml

    <select id="selectUser" resultType="User">
    SELECT * FROM user WHERE name = #{name} <!– 安全 –>
    </select>

    <select id="selectByTable" resultType="User">
    SELECT * FROM ${tableName} WHERE id = #{id} <!– 表名动态传入 –>
    </select>

    注意:使用 ${} 时,传入的参数值需要做好安全过滤(如白名单校验)。

    7.2 一级缓存失效场景

    问题:MyBatis 一级缓存什么时候会失效(被清空)?为什么执行 update 操作会清空缓存?

    答:
    一级缓存失效的场景包括:

  • 执行 insert、update、delete 操作(MyBatis 认为这些操作可能导致数据变化,因此清空缓存)。

  • 手动调用 sqlSession.clearCache()。

  • 执行 commit、rollback、close 操作(SqlSession 生命周期结束)。

  • 如果设置了 localCacheScope=STATEMENT,则每次查询后缓存会被清空(相当于只缓存当前语句)。

  • 为什么 update 会清空缓存:因为 MyBatis 无法知道 update 语句具体修改了哪些数据,为了保证数据一致性,最简单的做法就是清空该 SqlSession 的所有缓存。这也是一级缓存粒度粗的体现。

    7.3 二级缓存脏数据问题

    问题:请说明二级缓存可能产生脏数据的原因,以及如何避免。

    答:
    原因:二级缓存是 namespace 级别的,如果两个不同的 namespace 操作了同一张表(例如 UserMapper 和 OrderMapper 都涉及 user 表),但缓存却隔离,就会导致数据不一致。例如:

    • UserMapper 查询用户列表并缓存。

    • OrderMapper 更新了某个用户的订单(但不涉及用户表),不会触发 UserMapper 的缓存清空。

    • 之后再次通过 UserMapper 查询用户列表,返回的是旧缓存数据(用户信息可能已过时)。

    解决方案:

    • 避免在多 namespace 中使用二级缓存,或确保所有操作同一张表的 Mapper 都通过 <cache-ref> 共享同一个缓存区域。

    • 在更新操作时,手动清空相关 namespace 的缓存(不推荐)。

    • 使用更细粒度的缓存策略,如按主键缓存,但 MyBatis 二级缓存本身不支持这种细粒度(缓存 key 是整个查询语句+参数)。

    • 在分布式环境下,可采用集中式缓存(如 Redis),并通过缓存失效策略解决。

    7.4 MyBatis 的执行器类型

    问题:MyBatis 有哪几种执行器(Executor)?分别有什么特点?

    答:
    MyBatis 有三种内置执行器:

    • SIMPLE:默认执行器,每次执行 SQL 都会创建一个新的 PreparedStatement,执行完立即关闭。

    • REUSE:可重用执行器,会将 PreparedStatement 缓存起来(通过 Map<String, PreparedStatement>),下次执行同一条 SQL 时重用,减少预编译开销。

    • BATCH:批处理执行器,用于批量更新操作,将多个 SQL 语句批量发送到数据库执行,提高性能。但需要注意,BatchExecutor 在执行 query 或 commit 时会强制刷新批处理语句。

    可以在全局配置文件中通过 <setting name="defaultExecutorType" value="REUSE"/> 修改,也可以在创建 SqlSession 时指定。

    7.5 如何获取自增主键

    问题:在插入数据后,如何获取数据库自动生成的主键?

    答:
    有两种方式:

  • 使用 useGeneratedKeys 和 keyProperty(推荐):

    xml

    <insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO user(name) VALUES(#{name})
    </insert>

    执行后,传入的 user 对象的 id 属性会被自动赋值为生成的主键。

  • 使用 selectKey 标签(适用于非自增主键或复杂生成逻辑):

    xml

    <insert id="insertUser">
    <selectKey keyProperty="id" order="AFTER" resultType="int">
    SELECT LAST_INSERT_ID()
    </selectKey>
    INSERT INTO user(name) VALUES(#{name})
    </insert>

    order 属性可以是 BEFORE 或 AFTER,BEFORE 用于获取序列等场景。

  • 7.6 MyBatis 的延迟加载原理

    问题:MyBatis 的延迟加载是如何实现的?它有什么优缺点?

    答:
    实现原理:

    • MyBatis 通过创建目标对象的代理(CGLIB 或 Javassist)来实现延迟加载。

    • 当配置了延迟加载的关联属性(如 association 或 collection)时,MyBatis 会在映射时设置一个代理对象。

    • 第一次访问该属性时,代理对象会触发后续查询(执行预先定义的 SQL),从数据库加载数据并填充。

    配置:需要开启全局 lazyLoadingEnabled=true,并且关联查询中设置 fetchType="lazy"。

    优点:按需加载,避免一次性加载过多数据,提高性能。
    缺点:可能引发 N+1 查询问题(循环查询),需要在设计时权衡;同时延迟加载需要代理支持,可能带来少许性能开销。

    N+1 问题示例:
    查询所有用户(1 条 SQL),然后遍历每个用户获取其订单(N 条 SQL)。解决方案:使用关联查询一次性加载(join),或使用延迟加载的批量模式(配合 aggressiveLazyLoading 等)。

    7.7 MyBatis 的分页方式

    问题:MyBatis 有哪些分页实现方式?逻辑分页和物理分页有什么区别?

    答:

    • 逻辑分页:使用 MyBatis 自带的 RowBounds 对象,它在查询所有结果后,在内存中进行截取(offset, limit)。适合数据量小的场景,数据量大时性能差。

    • 物理分页:在 SQL 语句中添加 LIMIT 等数据库分页语法,只查询需要的数据。可通过手动拼接 SQL 或使用分页插件实现。

    • 分页插件:如 PageHelper,基于拦截器在 SQL 执行前动态拼接分页语句,并自动查询总记录数,对业务代码无侵入。

    区别:逻辑分页查询所有数据,消耗网络和内存;物理分页只查询部分数据,效率高。

    7.8 MyBatis 的批量操作

    问题:如何使用 MyBatis 进行高效的批量插入或更新?

    答:
    有两种方式:

  • 使用 BatchExecutor:
    通过 sqlSessionFactory.openSession(ExecutorType.BATCH) 获取 SqlSession,然后连续执行多次 insert/update 操作,最后 commit。MyBatis 会将多条 SQL 合并到一次网络请求中(取决于数据库驱动支持),提高效率。

  • 使用 foreach 拼接 SQL:
    在 Mapper XML 中,使用 <foreach> 一次性拼接多条 insert 语句,如:

    xml

    <insert id="batchInsert">
    INSERT INTO user(name) VALUES
    <foreach collection="list" item="item" separator=",">
    (#{item.name})
    </foreach>
    </insert>

    这种方式只需执行一次 SQL,效率更高,但注意 SQL 长度限制(max_allowed_packet)。

  • 推荐:对于大量数据,采用 BatchExecutor 方式,避免 SQL 过长;对于中小量数据,使用 foreach 方式简单高效。

    7.9 MyBatis 和 Spring 整合后的事务管理

    问题:在 Spring 中,如果 Service 方法上有 @Transactional 注解,MyBatis 的 SqlSession 是如何获取和管理的?

    答:

    • Spring 通过 SqlSessionTemplate 的 getSqlSessionFactory() 获取 SqlSession,内部通过 SqlSessionUtils 的 getSqlSession 方法获取当前事务绑定的 SqlSession。

    • 该方法会检查当前是否存在 Spring 事务(通过 TransactionSynchronizationManager 的资源绑定)。如果有,则从 ThreadLocal 中获取绑定的 SqlSession;如果没有,则创建新的 SqlSession,并在方法结束后自动关闭。

    • 同时,Spring 会将 MyBatis 的 SqlSession 包装成 SqlSessionInterceptor 的代理,实现事务同步:当 Spring 事务提交时,会通过事务同步机制调用 MyBatis 的 commit;回滚时调用 rollback。

    • 这样,一个事务内的所有 Mapper 操作都使用同一个 SqlSession,保证了事务的原子性和一致性。

    7.10 MyBatis 的二级缓存与 Redis 集成时,如何保证缓存一致?

    问题:当使用 Redis 作为二级缓存,多个应用实例同时操作数据库时,如何保证缓存的一致性?

    答:
    使用 Redis 作为二级缓存时,缓存是应用实例共享的,但仍然面临缓存与数据库的一致性问题。常见的解决方案有:

    • 设置合理的缓存过期时间,让缓存最终一致。

    • 更新数据库后主动清空相关缓存。可以在 Service 层手动删除 Redis 中的缓存键,或者在 MyBatis 的缓存实现中监听更新操作(但 MyBatis 二级缓存本身会在更新时调用 clear 方法,如果自定义 RedisCache 的 clear 方法实现为删除 namespace 下的所有键,则可达到目的)。

    • 使用缓存模式:如 Cache-Aside(旁路缓存),先更新数据库,再删除缓存;或者先删除缓存,再更新数据库(需处理并发问题)。

    • 对于强一致性要求高的场景,建议不要使用二级缓存,或者使用分布式事务等复杂方案。

    需要注意的是,MyBatis 的二级缓存是基于 namespace 的,如果采用自定义 RedisCache,clear 方法通常需要删除该 namespace 下的所有键,这可能导致缓存失效范围过大。一些优化方案如:使用多级缓存,或针对每个查询结果单独缓存键,但 MyBatis 本身的 CacheKey 设计已经包含了查询参数,因此更新操作无法精确知道哪些查询键需要失效,只能全清。


    8. 源码简析:从配置到执行

    为了更深入理解 MyBatis,我们简要过一下核心源码流程。

    8.1 构建 SqlSessionFactory

    java

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

    build 方法会创建 XMLConfigBuilder 解析配置文件,生成 Configuration 对象,然后返回 DefaultSqlSessionFactory。

    Configuration 是 MyBatis 的核心配置类,包含了所有配置信息(environment、mapper 注册、缓存等)。它内部维护了一个 Map<String, MappedStatement>,key 为 namespace + id,value 是封装了 SQL 语句、参数映射、结果映射等信息的 MappedStatement。

    8.2 获取 SqlSession 和 Mapper

    java

    SqlSession session = sqlSessionFactory.openSession();
    UserMapper mapper = session.getMapper(UserMapper.class);

    openSession 根据配置的 Executor 类型创建执行器,并包装事务。getMapper 通过 MapperProxyFactory 创建动态代理 MapperProxy。

    8.3 执行 Mapper 方法

    以 mapper.selectUser(1) 为例:

  • MapperProxy 的 invoke 方法被调用。

  • 根据方法名从 Configuration 中获取 MappedStatement。

  • 创建 Executor 执行查询。如果开启二级缓存,会先创建 CachingExecutor 包装原始 Executor。

  • 在 CachingExecutor 中,先根据 MappedStatement 的 cache 配置尝试从二级缓存获取结果。

  • 如果二级缓存没有命中,则调用实际 Executor(如 SimpleExecutor)的 query 方法。

  • SimpleExecutor 会创建 StatementHandler,并通过 ParameterHandler 设置参数,通过 ResultSetHandler 处理结果集。

  • 在执行过程中,一级缓存会通过 LocalCache 处理(在 BaseExecutor 中)。

  • 最终返回结果。

  • 8.4 缓存查找逻辑

    CachingExecutor 的 query 方法:

    java

    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    Cache cache = ms.getCache();
    if (cache != null) {
    // 判断是否需要刷新缓存
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
    // 从二级缓存获取
    List<E> list = (List<E>) cache.getObject(key);
    if (list == null) {
    list = delegate.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    cache.putObject(key, list);
    }
    return list;
    }
    }
    return delegate.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }

    BaseExecutor 的 query 方法处理一级缓存:

    java

    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // … 省略
    List<E> list = localCache.getObject(key);
    if (list != null) {
    handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
    list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
    return list;
    }

    queryFromDatabase 在查询后会放入 localCache。


    9. 性能优化与最佳实践

    9.1 SQL 层面优化

    • 合理使用索引,避免全表扫描。

    • 尽量减少关联查询,或使用冗余字段。

    • 使用批量操作减少数据库交互。

    • 对分页查询使用物理分页,并优化 count 查询。

    9.2 缓存优化

    • 合理使用一级缓存和二级缓存,避免脏数据。

    • 对于查询频率高、更新频率低的数据,开启二级缓存。

    • 二级缓存适合只读或读写比高的场景,注意序列化。

    9.3 插件使用

    • 使用分页插件统一处理分页逻辑。

    • 使用性能监控插件(如慢 SQL 日志)帮助优化。

    9.4 配置调优

    • 数据源连接池大小设置合理(根据并发数和数据库最大连接数)。

    • 调整 fetchSize 减少网络传输(大数据量查询时)。

    • 使用 REUSE 执行器减少 SQL 预编译开销。

    赞(0)
    未经允许不得转载:171主机测评 » MyBatis 一连串提问,被面试官吊打了!
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址