找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4746

积分

0

好友

650

主题
发表于 2 小时前 | 查看: 4| 回复: 0

一、基础概念与核心原理

1. 什么是 MyBatis?它与其他 ORM 框架(如 Hibernate)有什么区别?

MyBatis 是一款基于 Java 的半自动化 ORM(对象关系映射)框架,它通过 XML 或注解的方式将 SQL 语句与 Java 方法关联,实现对象与关系数据库的映射。与 Hibernate 等全自动 ORM 框架相比,主要区别在于:

  • SQL 控制粒度:MyBatis 允许开发者直接编写 SQL,灵活控制查询逻辑;Hibernate 通过 HQL 或 Criteria API 生成 SQL,屏蔽了底层 SQL 细节。
  • 学习成本:MyBatis 学习曲线平缓,适合 SQL 优化需求高的场景;Hibernate 初期学习成本高,但能快速开发简单 CRUD 功能。
  • 性能优化:MyBatis 可手动优化 SQL,适合复杂查询场景;Hibernate 在复杂查询时性能优化较困难。
  • 适用场景:MyBatis 适合需求多变、SQL 优化要求高的项目(如电商核心系统);Hibernate 适合快速开发、SQL 相对简单的项目。

面试点睛:重点强调 MyBatis 的 "半自动化" 特性 —— 既保留了 SQL 的灵活性,又提供了 ORM 的便利,这是它在企业级应用中广泛使用的核心原因。

2. MyBatis 的核心组件有哪些?各自的作用是什么?

MyBatis 的核心组件包括:

  1. SqlSessionFactory:会话工厂,负责创建 SqlSession,生命周期为应用级别(单例)。通常由 SqlSessionFactoryBuilder 根据配置文件构建。
  2. SqlSession:会话对象,代表与数据库的一次交互,包含了执行 SQL 的所有方法。生命周期为一次请求或事务,线程不安全,需及时关闭。
  3. Executor:执行器,SqlSession 的底层实现,负责 SQL 的执行和缓存管理。有三种类型:
    • SimpleExecutor:默认执行器,每次执行 SQL 都会创建新的 Statement
    • ReuseExecutor:复用 Statement
    • BatchExecutor:批量执行 SQL
  4. MappedStatement:映射语句,封装了 SQL 语句、参数类型、结果类型等信息,是 MyBatis 对 SQL 的抽象表示。
  5. StatementHandler:处理 JDBC 的 Statement 操作,负责参数设置、SQL 执行和结果集处理。
  6. ResultHandler:结果处理器,用于自定义结果集的处理逻辑。
  7. TypeHandler:类型处理器,负责 Java 类型与 JDBC 类型之间的转换。

面试点睛:这些组件的协作流程是面试重点 —— SqlSessionFactory 创建 SqlSessionSqlSession 通过 Executor 执行 MappedStatement,最终由 StatementHandler 与数据库交互。

3. MyBatis 的工作原理是什么?请简述其执行流程。

MyBatis 的工作流程可分为初始化和执行两个阶段:

初始化阶段:

  1. 加载 MyBatis 全局配置文件(mybatis-config.xml)和 Mapper 映射文件。
  2. 通过 SqlSessionFactoryBuilder 解析配置文件,生成 Configuration 对象(包含所有配置信息)。
  3. Configuration 创建 SqlSessionFactory 实例。

执行阶段:

  1. 调用 SqlSessionFactoryopenSession() 方法创建 SqlSession
  2. SqlSession 通过 Mapper 接口的全限定名 + 方法名找到对应的 MappedStatement
  3. Executor 根据 MappedStatement 的配置,通过 StatementHandler 执行 SQL:
    • ParameterHandler 处理参数绑定
    • 执行 SQL 语句
    • ResultSetHandler 处理结果集映射
  4. 事务提交或回滚。
  5. 关闭 SqlSession

流程图解:

配置文件 → SqlSessionFactoryBuilder → SqlSessionFactory → SqlSession → Executor → MappedStatement → 数据库

面试点睛:重点说明 Configuration 的核心作用(存储所有配置)和 MappedStatement 的作用(封装 SQL 信息),体现对框架设计的理解。

二、配置与映射

4. MyBatis 的核心配置文件(mybatis-config.xml)包含哪些主要配置项?

核心配置文件的主要配置项按顺序如下:

  • properties:加载外部属性文件(如数据库连接信息)。

    <properties resource="db.properties"/>
  • settings:全局配置参数,如缓存开关、延迟加载、日志实现等。

    <settings>
    <setting name="cacheEnabled" value="true"/>
    <setting name="lazyLoadingEnabled" value="true"/>
    </settings>
  • typeAliases:为 Java 类型设置别名,简化 Mapper 文件中的类型引用。

    <typeAliases>
    <typeAlias alias="User" type="com.example.entity.User"/>
    </typeAliases>
  • plugins:配置 MyBatis 插件(如分页插件、性能监控插件)。

    <plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor"/>
    </plugins>
  • environments:配置数据库环境,支持多环境切换。

    <environments default="dev">
    <environment id="dev">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="${jdbc.driver}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
        </dataSource>
    </environment>
    </environments>
  • mappers:注册 Mapper 映射文件或接口。

    <mappers>
    <mapper resource="com/example/mapper/UserMapper.xml"/>
    <package name="com.example.mapper"/>
    </mappers>

面试点睛:需说明配置项的顺序是固定的(MyBatis 解析时严格按顺序处理),且重点掌握 settingsmappers 的配置。

5. 如何理解 MyBatis 中的 Mapper 接口?它为什么不需要实现类?

MyBatis 的 Mapper 接口(也称映射器接口)是 SQL 操作的抽象定义,它与 Mapper.xml 文件或注解中的 SQL 语句绑定,无需手动实现。其底层原理是:

  • 动态代理:MyBatis 在运行时通过 JDK 动态代理为 Mapper 接口生成代理对象(MapperProxy)。
  • 方法映射:代理对象将接口方法调用转换为对 SqlSession 方法的调用,通过 "接口全限定名 + 方法名" 匹配对应的 MappedStatement
  • 参数传递:代理对象负责将接口方法的参数转换为 SQL 所需的参数类型。

关键源码片段:

// MapperProxy的invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
    }
    // 转换为MapperMethod执行
    MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
}

面试点睛:核心是动态代理机制,这也是 MyBatis "接口编程" 思想的体现,简化了数据访问层的代码。

6. resultMapresultType 的区别是什么?何时使用 resultMap

两者都是用于指定 SQL 查询结果的映射方式,主要区别如下:

特性 resultType resultMap
作用 直接指定返回值类型(JavaBean 或基本类型) 自定义结果映射规则,解决复杂映射问题
字段匹配 要求 SQL 查询列名与 JavaBean 属性名一致 支持列名与属性名映射、关联查询等
适用场景 简单查询、字段名与属性名一致的场景 复杂查询(关联查询、嵌套查询)、字段名与属性名不一致的场景

resultMap 示例(解决字段名与属性名不一致问题):

<resultMap id="userMap" type="User">
    <id column="user_id" property="id"/> <!-- 主键映射 -->
    <result column="user_name" property="username"/> <!-- 普通字段映射 -->
    <result column="create_time" property="createTime"/> <!-- 日期类型映射 -->
</resultMap>

<select id="selectUser" resultMap="userMap">
    SELECT user_id, user_name, create_time FROM t_user WHERE id = #{id}
</select>

使用建议:

  • 简单查询用 resultType,代码更简洁
  • 复杂映射(如多表关联、字段名不一致、集合映射)必须用 resultMap

面试点睛resultMap 是 MyBatis 映射能力的核心,能解决 resultType 无法处理的复杂场景,体现对 MyBatis 高级特性的掌握。

7. 动态 SQL 有哪些标签?请举例说明其用法。

MyBatis 的动态 SQL 用于根据条件动态生成 SQL 语句,核心标签包括:

  • if:条件判断,满足条件则包含标签内的 SQL。

    <select id="selectUser" resultType="User">
    SELECT * FROM user
    WHERE 1=1
    <if test="username != null">AND username LIKE CONCAT('%', #{username}, '%')</if>
    <if test="status != null">AND status = #{status}</if>
    </select>
  • where:替代 WHERE 关键字,自动处理 AND/OR 前缀。

    <select id="selectUser" resultType="User">
    SELECT * FROM user
    <where>
        <if test="username != null">AND username LIKE CONCAT('%', #{username}, '%')</if>
        <if test="status != null">AND status = #{status}</if>
    </where>
    </select>
  • choose/when/otherwise:多条件分支判断(类似 switch-case)。

    <select id="selectUser" resultType="User">
    SELECT * FROM user
    <where>
        <choose>
            <when test="id != null">AND id = #{id}</when>
            <when test="username != null">AND username = #{username}</when>
            <otherwise>AND status = 1</otherwise>
        </choose>
    </where>
    </select>
  • foreach:遍历集合,常用于 IN 条件或批量操作。

    <select id="selectByIds" resultType="User">
    SELECT * FROM user WHERE id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
    </select>
  • set:用于 UPDATE 语句,自动处理逗号。

    <update id="updateUser">
    UPDATE user
    <set>
        <if test="username != null">username = #{username},</if>
        <if test="email != null">email = #{email},</if>
    </set>
    WHERE id = #{id}
    </update>
  • trim:自定义字符串截取规则,可替代 where/set

    <trim prefix="WHERE" prefixOverrides="AND|OR">
    <!-- 内容 -->
    </trim>

面试点睛:动态 SQL 是 MyBatis 的强大特性,能避免手动拼接 SQL 的风险,需重点掌握 ifwhereforeach 的用法及场景。

三、SQL 执行与事务

8. MyBatis 的 SqlSession 有哪些常用方法?它是线程安全的吗?

SqlSession 是 MyBatis 与数据库交互的核心接口,常用方法包括:

  • 查询方法

    <T> T selectOne(String statement, Object parameter):查询单个结果
    List<T> selectList(String statement, Object parameter):查询集合
    Map<K, V> selectMap(String statement, Object parameter, String mapKey):查询结果映射为 Map
  • 插入方法

    int insert(String statement, Object parameter):执行插入,返回影响行数
  • 更新方法

    int update(String statement, Object parameter):执行更新,返回影响行数
  • 删除方法

    int delete(String statement, Object parameter):执行删除,返回影响行数
  • 事务方法

    void commit():提交事务
    void rollback():回滚事务
  • 获取 Mapper

    <T> T getMapper(Class<T> type):获取 Mapper 接口代理对象

线程安全性:

SqlSession 不是线程安全的,其设计为一次请求或一个事务的生命周期。原因是:

  • SqlSession 内部持有 Connection 对象,而 Connection 是非线程安全的
  • 多个线程共享 SqlSession 会导致事务管理混乱和数据不一致

最佳实践:

  • 在 Spring 环境中,通过 @Autowired 注入的 Mapper 接口由 Spring 管理,无需手动处理 SqlSession
  • 非 Spring 环境中,应在方法内部创建 SqlSession,使用后立即关闭(try-with-resources)

面试点睛:线程安全性是高频考点,需明确说明 SqlSession 不可共享,并解释原因。

9. MyBatis 的 Executor 有哪些类型?它们的区别是什么?

Executor 是 MyBatis 的核心执行器,负责 SQL 执行和缓存管理,主要有三种类型:

  1. SimpleExecutor

    • 默认执行器,每次执行 SQL 都会创建新的 StatementPreparedStatement/Statement
    • 执行后关闭 Statement,适用于大多数场景
    • 优点:简单直观;缺点:频繁创建和关闭 Statement,性能略低
  2. ReuseExecutor

    • 复用 Statement,根据 SQL 语句的 hash 值缓存 Statement
    • 同一 SQL 语句重复执行时,直接复用已创建的 Statement
    • 优点:减少 Statement 创建开销;缺点:缓存 Statement 会占用一定内存
  3. BatchExecutor

    • 批量执行 SQL,将多个 INSERT/UPDATE/DELETE 操作缓存,统一提交
    • 调用 SqlSession.flushStatements()commit() 时执行批量操作
    • 优点:大幅提升批量操作性能;缺点:只适用于批量写入场景

配置方式:

<settings>
    <setting name="defaultExecutorType" value="SIMPLE"/> <!-- 默认为SIMPLE -->
</settings>

或在创建 SqlSession 时指定:

SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);

适用场景:

  • 普通查询:SimpleExecutor
  • 重复执行相同 SQL:ReuseExecutor
  • 批量插入 / 更新:BatchExecutor

面试点睛BatchExecutor 的使用场景和原理是重点,体现对性能优化的理解。

10. MyBatis 如何处理事务?它与 Spring 事务如何整合?

MyBatis 自身的事务管理基于 JDBC 的事务机制,核心实现如下:

事务管理类型:

  • JDBC:依赖数据库的事务支持,通过 Connectioncommit/rollback 实现
  • MANAGED:将事务管理交给容器(如 Spring),MyBatis 不参与事务控制

默认行为:

  • SqlSession 默认开启事务(autoCommit=false
  • 需手动调用 commit() 提交事务,或 rollback() 回滚事务
  • 关闭 SqlSession 时若未提交,会自动回滚

与 Spring 事务整合:

企业级应用中通常使用 Spring 的声明式事务管理,整合步骤:

  • 配置数据源:使用 Spring 管理的数据源,确保 MyBatis 与 Spring 共享同一 Connection

    @Bean
    public DataSource dataSource() {
    // 配置数据源(如HikariCP)
    }
  • 配置 SqlSessionFactory:使用 Spring 提供的 SqlSessionFactoryBean,注入数据源

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
    factory.setDataSource(dataSource);
    // 其他配置(如Mapper位置)
    return factory.getObject();
    }
  • 配置事务管理器:使用 DataSourceTransactionManager

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
    }
  • 启用声明式事务:通过 @EnableTransactionManagement 注解

    @Configuration
    @EnableTransactionManagement
    public class AppConfig { ... }
  • 使用 @Transactional 注解:在 Service 方法上声明事务属性

    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Order order) {
    // 业务逻辑
    }

整合原理:

Spring 通过 AOP 为标注 @Transactional 的方法创建代理,在方法执行前开启事务,执行后根据是否异常决定提交或回滚,确保 MyBatis 的 SqlSession 与 Spring 事务使用同一 Connection

面试点睛:重点说明整合的核心是共享数据源和 Connection,以及 Spring 事务管理器如何接管 MyBatis 的事务控制。

四、缓存机制

11. MyBatis 的一级缓存和二级缓存有什么区别?如何配置和使用?

MyBatis 提供两级缓存机制,用于减少数据库访问,提升性能:

特性 一级缓存 二级缓存
作用范围 SqlSession 级别(同一个会话) Mapper 接口级别(同一个 namespace)
默认状态 默认开启,无法关闭 默认关闭,需手动开启
缓存键 SqlSession + MappedStatement + 参数 Mapper namespace + MappedStatement + 参数
失效时机 SqlSession 关闭或执行写操作(insert/update/delete 同一 namespace 执行写操作,或缓存过期
存储介质 内存 内存(可配置第三方缓存如 Redis)

一级缓存示例:

try (SqlSession session = sqlSessionFactory.openSession()) {
    UserMapper mapper = session.getMapper(UserMapper.class);
    // 第一次查询:从数据库获取
    User user1 = mapper.selectById(1);
    // 第二次查询:命中一级缓存
    User user2 = mapper.selectById(1);
    System.out.println(user1 == user2); // true(同一对象)
}

二级缓存配置与使用:

  1. 全局开启二级缓存

    <settings>
    <setting name="cacheEnabled" value="true"/> <!-- 默认true,可省略 -->
    </settings>
  2. Mapper.xml 中配置缓存

    <mapper namespace="com.example.mapper.UserMapper">
    <!-- 开启二级缓存 -->
    <cache
        eviction="LRU"  <!-- 淘汰策略:LRU(最近最少使用) -->
        flushInterval="60000"  <!-- 自动刷新时间(毫秒) -->
        size="1024"  <!-- 最大缓存对象数 -->
        readOnly="false"/>
    
    <!-- 配置SQL使用缓存 -->
    <select id="selectById" resultType="User" useCache="true">
        SELECT * FROM user WHERE id = #{id}
    </select>
    
    <!-- 写操作刷新缓存 -->
    <update id="updateById" flushCache="true">
        UPDATE user SET username = #{username} WHERE id = #{id}
    </update>
    </mapper>
  3. 实体类实现序列化

    public class User implements Serializable { ... }

使用建议:

  • 一级缓存无需手动配置,注意 SqlSession 的生命周期即可
  • 二级缓存适用于查询频繁、更新较少的数据(如字典表)
  • 关联查询多的表不建议使用二级缓存,可能导致数据不一致

面试点睛:需明确两级缓存的作用范围和失效机制,以及二级缓存的适用场景限制。

12. 如何整合 Redis 作为 MyBatis 的二级缓存?

MyBatis 默认的二级缓存基于内存,不适合分布式环境,整合 Redis 作为二级缓存的步骤如下:

  • 引入依赖

    <dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>1.0.0-beta2</version>
    </dependency>
    <dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.7.0</version>
    </dependency>
  • 配置 Redis 缓存
    src/main/resources 下创建 redis.properties

    redis.host=localhost
    redis.port=6379
    redis.timeout=2000
    redis.password=
    redis.database=0
    redis.keyPrefix=mybatis:cache:
    redis.expire=3600  # 缓存过期时间(秒)
  • 在 Mapper 中使用 Redis 缓存

    <mapper namespace="com.example.mapper.DictMapper">
    <!-- 指定使用Redis缓存 -->
    <cache type="org.mybatis.caches.redis.RedisCache"/>
    
    <!-- 查询方法使用缓存 -->
    <select id="selectByType" resultType="Dict" useCache="true">
        SELECT * FROM dict WHERE type = #{type}
    </select>
    </mapper>
  • 自定义 Redis 缓存(可选)
    如需自定义序列化方式或缓存逻辑,可继承 RedisCache

    public class CustomRedisCache extends RedisCache {
    public CustomRedisCache(String id) {
        super(id);
    }
    
    @Override
    public void putObject(Object key, Object value) {
        // 自定义存入逻辑(如使用JSON序列化)
        super.putObject(key, value);
    }
    
    @Override
    public Object getObject(Object key) {
        // 自定义获取逻辑
        return super.getObject(key);
    }
    }

并在 Mapper 中引用:

<cache type="com.example.cache.CustomRedisCache"/>

整合原理:

MyBatis 的缓存接口 Cache 是扩展点,RedisCache 实现了该接口,将缓存数据存储到 Redis 而非内存,实现分布式环境下的缓存共享。

面试点睛:重点说明 MyBatis 缓存的扩展机制(通过实现 Cache 接口),以及分布式缓存的必要性。

五、高级特性与源码分析

13. MyBatis 的插件机制是什么?如何实现一个自定义插件(如分页插件)?

MyBatis 的插件机制基于拦截器模式,允许在 SQL 执行过程中插入自定义逻辑,可拦截的四大核心对象包括:

  • Executor:执行器(update/query/flushStatements/commit/rollback 等方法)
  • ParameterHandler:参数处理器(getParameterObject/setParameters 方法)
  • ResultSetHandler:结果集处理器(handleResultSets/handleOutputParameters 方法)
  • StatementHandler:语句处理器(prepare/parameterize/batch/update/query 方法)

实现自定义分页插件的步骤:

  • 实现 Interceptor 接口

    @Intercepts({
    @Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
    )
    })
    public class PageInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 获取StatementHandler
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
    
        // 2. 获取当前SQL信息
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        String sql = (String) metaObject.getValue("delegate.boundSql.sql");
    
        // 3. 判断是否需要分页(假设参数中包含Page对象)
        Object parameter = statementHandler.getParameterHandler().getParameterObject();
        if (parameter instanceof Page) {
            Page<?> page = (Page<?>) parameter;
            // 4. 重写SQL,添加分页条件(以MySQL为例)
            String pageSql = sql + " LIMIT " + page.getOffset() + ", " + page.getPageSize();
            metaObject.setValue("delegate.boundSql.sql", pageSql);
        }
    
        // 5. 执行原方法
        return invocation.proceed();
    }
    
    @Override
    public Object plugin(Object target) {
        // 生成代理对象
        return Plugin.wrap(target, this);
    }
    
    @Override
    public void setProperties(Properties properties) {
        // 读取插件配置参数
    }
    }
  • 配置插件

    <plugins>
    <plugin interceptor="com.example.plugin.PageInterceptor">
        <!-- 可选配置参数 -->
        <property name="dialect" value="mysql"/>
    </plugin>
    </plugins>
  • 使用分页插件

    Page<User> page = new Page<>(1, 10); // 第1页,每页10条
    List<User> users = userMapper.selectByPage(page);
    long total = page.getTotal(); // 总条数(需额外查询count)

插件执行原理:

MyBatis 通过 JDK 动态代理为被拦截对象生成代理,当调用被拦截方法时,会先执行插件的 intercept 方法,再通过 invocation.proceed() 执行原方法。

面试点睛:需说明插件可拦截的对象和方法,以及拦截器的实现要点(@Intercepts 注解、Plugin.wrap 方法)。

14. MyBatis 的延迟加载(懒加载)原理是什么?如何配置?

延迟加载是 MyBatis 的关联查询优化机制,指在查询主对象时不立即加载关联对象,而是在真正使用关联对象时才执行查询,减少不必要的数据库访问。

实现原理:

MyBatis 通过 CGLIB 或 JDK 动态代理为关联对象创建代理对象,当调用关联对象的 getter 方法时,触发代理逻辑,执行关联查询 SQL。

配置方式:

  • 全局开启懒加载

    <settings>
    <setting name="lazyLoadingEnabled" value="true"/> <!-- 开启懒加载 -->
    <setting name="aggressiveLazyLoading" value="false"/> <!-- 关闭积极加载(按需加载) -->
    </settings>
  • resultMap 中配置关联查询

    <!-- 订单结果映射 -->
    <resultMap id="orderMap" type="Order">
    <id column="id" property="id"/>
    <result column="order_no" property="orderNo"/>
    <!-- 懒加载订单项(一对多) -->
    <collection
        property="items"
        ofType="OrderItem"
        select="com.example.mapper.OrderItemMapper.selectByOrderId"
        column="id"
        fetchType="lazy"/> <!-- 显式指定懒加载 -->
    </resultMap>
    <select id="selectOrder" resultMap="orderMap">
    SELECT id, order_no FROM `order` WHERE id = #{id}
    </select>
  • 订单项查询

    <mapper namespace="com.example.mapper.OrderItemMapper">
    <select id="selectByOrderId" resultType="OrderItem">
        SELECT * FROM order_item WHERE order_id = #{orderId}
    </select>
    </mapper>

使用效果:

Order order = orderMapper.selectOrder(1L);
// 此时未查询订单项,只查询了订单主表
System.out.println(order.getOrderNo()); // 不触发关联查询
List<OrderItem> items = order.getItems(); 
// 调用getItems()时,触发懒加载,执行selectByOrderId查询
items.forEach(System.out::println);

注意事项:

  • 懒加载仅适用于关联查询(association/collection
  • 必须在 SqlSession 生命周期内使用懒加载的关联对象,否则会抛出异常
  • 可通过 fetchType="eager" 为特定关联配置立即加载

面试点睛:核心是动态代理机制,需说明懒加载的触发时机和配置要点,以及与 N+1 查询问题的关系。

15. MyBatis 的 Mapper 代理是如何实现的?请结合源码说明。

MyBatis 的 Mapper 代理通过 JDK 动态代理实现,核心是 MapperProxy 类,其工作流程如下:

  • 获取 Mapper 接口代理对象
    当调用 SqlSession.getMapper(Class<T> type) 时,MyBatis 通过 MapperProxyFactory 创建代理对象。

    // MapperRegistry类
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    return mapperProxyFactory.newInstance(sqlSession);
    }
    // MapperProxyFactory类
    public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
    }
    protected T newInstance(MapperProxy<T> mapperProxy) {
    // 创建JDK动态代理
    return (T) Proxy.newProxyInstance(
        mapperInterface.getClassLoader(),
        new Class[] { mapperInterface },
        mapperProxy
    );
    }
  • 代理对象执行方法
    当调用 Mapper 接口方法时,会触发 MapperProxyinvoke 方法。

    // MapperProxy类
    @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);
    }
    // 处理接口默认方法
    if (method.isDefault()) {
        return invokeDefaultMethod(proxy, method, args);
    }
    // 处理Mapper接口方法
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
    }
  • 方法执行映射
    MapperMethod 将接口方法映射为 SqlSession 的对应操作。

    // MapperMethod类
    public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
        case INSERT: {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.insert(command.getName(), param));
            break;
        }
        case UPDATE: {
            // 处理更新
        }
        case DELETE: {
            // 处理删除
        }
        case SELECT:
            if (method.returnsVoid() && method.hasResultHandler()) {
                // 处理带ResultHandler的查询
            } else if (method.returnsMany()) {
                result = executeForMany(sqlSession, args); // 处理集合查询
            } else if (method.returnsMap()) {
                // 处理Map查询
            } else {
                // 处理单个对象查询
            }
            break;
        case FLUSH:
            result = sqlSession.flushStatements();
            break;
        default:
            throw new BindingException("Unknown execution method for: " + command.getName());
    }
    // 处理返回结果
    return result;
    }

核心原理:

通过 "接口全限定名 + 方法名" 与 Mapper.xml 中 SQL 的 id 进行匹配,将接口方法调用转换为对 SqlSession 相应方法的调用,实现无侵入式的 SQL 执行。

面试点睛:这是 MyBatis 的核心设计,需结合动态代理和方法映射的过程说明,体现对框架底层的理解。

六、实战问题与性能优化

16. 什么是 N+1 查询问题?如何解决?

N+1 查询问题是 MyBatis 关联查询中常见的性能陷阱,指查询 N 条主表数据后,每条主表数据又触发一次子表查询,导致总共 N+1 次数据库交互。

问题示例:

// 1. 查询所有订单(1次查询)
List<Order> orders = orderMapper.selectAll();
// 2. 遍历订单,查询每个订单的明细(N次查询)
for (Order order : orders) {
    List<OrderItem> items = orderItemMapper.selectByOrderId(order.getId());
    order.setItems(items);
}
// 总计:1 + N 次查询

解决方案:

  • 关联查询一次性加载
    使用 associationcollection 进行 JOIN 查询,一次性获取所有数据。
    
    <resultMap id="orderWithItemsMap" type="Order">
    <id column="id" property="id"/>
    <result column="order_no" property="orderNo"/>
    <collection property="items" ofType="OrderItem">
        <id column="item_id" property="id"/>
        <result column="product_id" property="productId"/>
    </collection>
    </resultMap>

<select id="selectOrdersWithItems" resultMap="orderWithItemsMap">
SELECT
o.id, o.order_no,
oi.id AS item_id, oi.product_id
FROM order o
LEFT JOIN order_item oi ON o.id = oi.order_id
</select>


- **延迟加载 + 批量查询**:
结合懒加载和 `foreach` 实现 "1+1" 次查询。
```xml
<!-- 订单映射(懒加载) -->
<resultMap id="orderMap" type="Order">
    <id column="id" property="id"/>
    <collection
        property="items"
        select="com.example.mapper.OrderItemMapper.selectByOrderIds"
        column="id"
        fetchType="lazy"/>
</resultMap>

<!-- 订单项批量查询 -->
<select id="selectByOrderIds" resultType="OrderItem">
    SELECT * FROM order_item WHERE order_id IN
    <foreach collection="list" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>
  • 使用 MyBatis-Plus 的关联查询
    利用 MyBatis-Plus 的 @TableName@TableField 注解简化关联查询。

面试点睛:需说明 N+1 问题的成因和两种解决方案的适用场景(关联查询适合数据量小的场景,延迟加载 + 批量查询适合数据量大的场景)。

17. MyBatis 有哪些常见的性能优化手段?

MyBatis 的性能优化需从 SQL、缓存、连接池等多方面入手,常见手段包括:

  • SQL 优化

    • 避免使用 SELECT *,只查询必要字段
    • 优化 JOIN 操作,控制关联表数量(不超过 3 张)
    • 使用索引优化查询条件,避免全表扫描
    • 分页查询限制返回数据量,避免大数据集加载
  • 缓存优化

    • 合理使用一级缓存(控制 SqlSession 生命周期)
    • 对热点数据启用二级缓存或分布式缓存(如 Redis)
    • 设置合理的缓存过期时间,避免缓存雪崩
  • 连接池优化

    • 使用性能优异的连接池(如 HikariCP)
    • 合理配置连接池参数(最大连接数、等待时间等)
      <dataSource type="POOLED">
      <property name="poolMaximumActiveConnections" value="20"/> <!-- 最大活跃连接 -->
      <property name="poolMaximumIdleConnections" value="10"/> <!-- 最大空闲连接 -->
      <property name="poolMaximumCheckoutTime" value="20000"/> <!-- 最大 checkout 时间 -->
      </dataSource>
  • 批量操作优化

    • 使用 BatchExecutor 执行批量插入 / 更新
    • 避免循环中执行单条 SQL,改用 foreach 批量操作
      <insert id="batchInsert">
      INSERT INTO user (username, email) VALUES
      <foreach collection="list" item="user" separator=",">
      (#{user.username}, #{user.email})
      </foreach>
      </insert>
  • 其他优化

    • 启用延迟加载,减少不必要的关联查询
    • 使用 resultMap 代替 resultType,避免字段映射开销
    • 合理使用 fetchSize 控制 JDBC 的每次抓取行数
    • 监控慢查询,使用 EXPLAIN 分析并优化

面试点睛:性能优化是企业级应用的重点,需结合具体场景说明优化手段,体现实战经验。

18. MyBatis 如何处理存储过程?请举例说明。

MyBatis 支持调用数据库存储过程,通过 statementType="CALLABLE" 配置,并使用 #{parameterMode=IN/OUT/INOUT} 指定参数模式。

示例(MySQL 存储过程):

  • 创建存储过程

    -- 根据用户ID查询用户信息,并返回总记录数
    DELIMITER //
    CREATE PROCEDURE select_user(
    IN userId INT,
    OUT total INT
    )
    BEGIN
    SELECT COUNT(1) INTO total FROM user;
    SELECT * FROM user WHERE id = userId;
    END //
    DELIMITER ;
  • Mapper.xml 配置

    <select id="callSelectUser" statementType="CALLABLE" resultType="User">
    {
        call select_user(
            #{userId, mode=IN, jdbcType=INTEGER},
            #{total, mode=OUT, jdbcType=INTEGER}
        )
    }
    </select>
  • Mapper 接口

    public interface UserMapper {
    User callSelectUser(Map<String, Object> params);
    }
  • 调用存储过程

    Map<String, Object> params = new HashMap<>();
    params.put("userId", 1);
    // 执行存储过程
    User user = userMapper.callSelectUser(params);
    // 获取OUT参数
    Integer total = (Integer) params.get("total");

处理结果集的存储过程:

对于返回多个结果集的存储过程,需使用 resultSets 属性指定结果集映射:

<select id="callMultiResultProc" statementType="CALLABLE"
        resultSets="users,orders"
        resultMap="userMap,orderMap">
    {call get_user_orders(#{userId, mode=IN})}
</select>

七、总结与面试建议

MyBatis 作为企业级应用的主流 ORM 框架,其面试考察点涵盖基础概念、核心原理、配置使用、性能优化和源码理解等多个层面。准备面试时,建议:

  1. 夯实基础:熟练掌握核心组件、配置文件、映射规则和动态 SQL。
  2. 理解原理:深入理解 Mapper 代理、SQL 执行流程、缓存机制等底层实现。
  3. 注重实战:掌握事务管理、批量操作、存储过程调用等实际应用场景。
  4. 关注性能:能分析和解决 N+1 查询、慢查询等性能问题。
  5. 阅读源码:重点理解 SqlSessionExecutorMapperProxy 等核心类的实现。

通过系统学习这些知识点,并结合实际项目经验,就能在 MyBatis 相关面试中脱颖而出。




上一篇:支付系统风控架构实战:事前事中事后全流程设计与核心指标解析
下一篇:微软开源Magentic UI:基于人机协作的智能体工作流解析与实践
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-11 10:41 , Processed in 0.703222 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表