MyBatis源码与性能优化深度解析:把ORM框架核心讲透,吊打面试官

0 阅读11分钟

MyBatis源码与性能优化深度解析:把ORM框架核心讲透,吊打面试官

🎯 写在前面:MyBatis是Java ORM框架的经典之作。相比JPA,MyBatis更加灵活可控;相比JDBC,MyBatis又屏蔽了大量样板代码。但你真的了解MyBatis的底层原理吗?这篇文章,将带你从源码层面深度剖析MyBatis!

一、MyBatis核心架构:一次请求的完整流程

1.1 整体架构图

┌─────────────────────────────────────────────────────────────────────┐
│                      MyBatis整体架构                                │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  ┌────────────────────────────────────────────────────────────────┐ │
│  │                     API层(SqlSession)                         │ │
│  │   SqlSessionFactory → SqlSession → Mapper Proxy                │ │
│  └────────────────────────────────────────────────────────────────┘ │
│                              ↓                                        │
│  ┌────────────────────────────────────────────────────────────────┐ │
│  │                     核心处理层                                  │ │
│  │   Executor → StatementHandler → ParameterHandler → ResultSetHandler │
│  └────────────────────────────────────────────────────────────────┘ │
│                              ↓                                        │
│  ┌────────────────────────────────────────────────────────────────┐ │
│  │                     基础支撑层                                  │ │
│  │   TransactionManager | ConnectionPool | Cache(一级/二级)        │ │
│  └────────────────────────────────────────────────────────────────┘ │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

1.2 一次查询的完整流程

// 1. 获取SqlSession(门面入口)
SqlSession sqlSession = sqlSessionFactory.openSession();

// 2. 获取Mapper代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

// 3. 调用接口方法
User user = userMapper.findById(1L);

// 4. 底层原理:jdk动态代理
// MyBatis使用MapperProxyFactory为每个Mapper接口创建代理对象
// 代理对象的invoke方法执行以下逻辑:
//   ① 从Configuration中获取MappedStatement
//   ② 调用Executor执行SQL
//   ③ 处理参数
//   ④ 处理结果集

1.3 核心组件详解

/**
 * 核心组件及其职责
 */
public class MyBatisCoreComponents {
    
    // 1. SqlSessionFactoryBuilder:构建SqlSessionFactory
    // 负责解析mybatis-config.xml和mapper.xml,生成Configuration
    
    // 2. SqlSessionFactory:SqlSession工厂
    // 负责创建SqlSession实例
    
    // 3. SqlSession:MyBatis的门面接口
    // 提供了所有操作数据库的方法:selectOne, selectList, insert, update, delete
    
    // 4. Executor:执行器(核心)
    // - SimpleExecutor:简单执行器
    // - ReuseExecutor:复用执行器(缓存Statement)
    // - BatchExecutor:批量执行器(批量操作)
    // - CachingExecutor:缓存执行器(装饰器模式,包装以上三种)
    
    // 5. StatementHandler:语句处理器
    // - PreparedStatementHandler:预编译SQL(防止SQL注入)
    // - SimpleStatementHandler:简单SQL
    // - CallableStatementHandler:存储过程
    
    // 6. ParameterHandler:参数处理器
    // 将Java对象转换为SQL参数
    
    // 7. ResultSetHandler:结果集处理器
    // 将ResultSet转换为Java对象
    
    // 8. TypeHandler:类型转换器
    // JDBC类型 ↔ Java类型 之间的转换
}

二、SQL执行原理:Executor深度剖析

2.1 Executor家族

┌─────────────────────────────────────────────────────────────────────┐
│                      Executor执行器体系                              │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│                      Executor(接口)                                 │
│                            ↑                                         │
│              ┌───────────┴───────────┐                               │
│              │                       │                               │
│     BaseExecutor              CachingExecutor                        │
│              │                       │                               │
│    ┌─────────┼─────────┐       ┌──────┴──────┐                        │
│    ↓         ↓         ↓       ↓             ↓                       │
│  Simple   Reuse    Batch   ←── 包装 ──→  其他执行器                   │
│ Executor Executor Executor                                                 │
│                                                                      │
│  职责:                                                                │
│  - 管理Statement生命周期                                              │
│  - 一级缓存管理(BaseExecutor)                                       │
│  - 事务管理                                                           │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

2.2 执行流程源码解析

// 核心流程:SqlSession.selectList()
public class SqlSession selectList(String statement, Object parameter) {
    // 1. 从Configuration获取MappedStatement
    MappedStatement ms = configuration.getMappedStatement(statement);
    
    // 2. 调用Executor执行
    return executor.query(ms, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
}

// BaseExecutor.query()
public class <E> List<E> query(MappedStatement ms, Object parameter, 
                               RowBounds rowBounds, ResultHandler resultHandler) {
    
    // 1. 创建BoundSql(包含最终SQL和参数)
    BoundSql boundSql = ms.getBoundSql(parameter);
    
    // 2. 生成缓存Key(一级缓存的key)
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    
    // 3. 查询(一级缓存)
    List<E> list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    
    return list;
}

private <E> List<E> queryFromDatabase(...) {
    // 1. 先查询一级缓存
    List<E> list = localCache.getObject(key);
    if (list != null) {
        return list;  // 缓存命中
    }
    
    // 2. 缓存未命中,查询数据库
    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    
    // 3. 放入一级缓存
    localCache.putObject(key, list);
    
    return list;
}

// CachingExecutor.query()(二级缓存)
public <E> List<E> query(MappedStatement ms, Object parameter, ...) {
    // 1. 查询二级缓存
    Cache cache = ms.getCache();
    if (cache != null) {
        flushCacheIfRequired(ms);  // 根据flushCache配置决定是否清空
        CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
        List<E> list = cache.getObject(key);
        if (list != null) {
            return list;  // 二级缓存命中
        }
    }
    
    // 2. 调用被包装的执行器查询
    List<E> list = delegate.query(ms, parameter, rowBounds, resultHandler, boundSql);
    
    // 3. 放入二级缓存
    if (cache != null) {
        cache.putObject(key, list);
    }
    
    return list;
}

三、缓存机制:二级缓存避坑指南

3.1 一级缓存 vs 二级缓存

┌─────────────────────────────────────────────────────────────────────┐
│                      MyBatis缓存机制                                 │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  ┌────────────────────────────────────────────────────────────────┐ │
│  │                     一级缓存(SqlSession级)                     │ │
│  │                                                                 │ │
│  │  作用域:SqlSession                                              │ │
│  │  存储介质: PerpetualCache(HashMap)                            │ │
│  │  生命周期: SqlSession关闭后清空                                  │ │
│  │  默认开启: 是                                                   │ │
│  │  所属组件: BaseExecutor                                        │ │
│  │                                                                 │ │
│  │  缓存失效场景:                                                   │ │
│  │  ❌ 不同的SqlSession                                             │ │
│  │  ❌ 查询条件不同                                                 │ │
│  │  ❌ 执行了增删改操作(会清空该SqlSession的缓存)                   │ │
│  │  ❌ 手动调用clearCache()                                         │ │
│  │  ❌ 事务回滚                                                     │ │
│  └────────────────────────────────────────────────────────────────┘ │
│                              ↓                                        │
│  ┌────────────────────────────────────────────────────────────────┐ │
│  │                     二级缓存(Mapper级)                        │ │
│  │                                                                 │ │
│  │  作用域:Mapper级别,整个应用共享                                │ │
│  │  存储介质:可配置(PerpetualCache、Ehcache、Redis等)            │ │
│  │  生命周期: 应用运行期间                                         │ │
│  │  默认开启: 否(需要配置<cache>)                                │ │
│  │  所属组件: CachingExecutor                                      │ │
│  │                                                                 │ │
│  │  特点:                                                          │ │
│  │  ✅ 以Mapper为namespace,隔离不同Mapper的缓存                    │ │
│  │  ✅ 事务提交后才写入(需要SqlSession提交/close)                  │ │
│  │  ⚠️ 与一级缓存配合使用                                           │ │
│  └────────────────────────────────────────────────────────────────┘ │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

3.2 二级缓存配置与使用

<!-- mapper.xml中开启二级缓存 -->
<mapper namespace="com.example.mapper.UserMapper">
    
    <!-- 
        eviction: 淘汰策略
        - LRU: 最近最少使用(默认)
        - FIFO: 先进先出
        - SOFT: 软引用
        - WEAK: 弱引用
        
        flushInterval: 刷新间隔(毫秒),不设置则不刷新
        readOnly: 是否只读
        size: 缓存对象数量
    -->
    <cache 
        eviction="LRU" 
        flushInterval="60000" 
        readOnly="false" 
        size="512"
    />
    
    <!-- 查询结果放入二级缓存 -->
    <select id="findById" resultType="User" useCache="true">
        SELECT * FROM user WHERE id = #{id}
    </select>
    
    <!-- 增删改操作清空二级缓存 -->
    <insert id="insert" useGeneratedKeys="true">
        INSERT INTO user(name, email) VALUES(#{name}, #{email})
    </insert>
    
</mapper>
// 二级缓存与事务配合
@Service
public class UserService {
    
    @Autowired
    private SqlSessionFactory sqlSessionFactory;
    
    // 场景1:同一个SqlSession,两次查询(命中一级缓存)
    public void queryWithSameSession() {
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            
            User user1 = mapper.findById(1L);  // 查询数据库
            User user2 = mapper.findById(1L);  // 一级缓存命中
            
            // 两个对象是同一个吗?是的!(同一个SqlSession)
            System.out.println(user1 == user2);  // true
        }
    }
    
    // 场景2:不同SqlSession,两次查询(命中二级缓存)
    public void queryWithDifferentSessions() {
        // 第一个SqlSession
        try (SqlSession sqlSession1 = sqlSessionFactory.openSession()) {
            UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
            mapper1.findById(1L);  // 查询数据库
            sqlSession1.commit();  // 重要!提交后才写入二级缓存
        }
        
        // 第二个SqlSession
        try (SqlSession sqlSession2 = sqlSessionFactory.openSession()) {
            UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
            mapper2.findById(1L);  // 二级缓存命中
        }
    }
}

3.3 缓存连环坑

// 坑1:关联查询缓存问题
// userMapper.xml
<select id="findUserWithOrders" resultMap="UserOrderMap">
    SELECT u.*, o.* FROM user u 
    LEFT JOIN orders o ON u.id = o.user_id
</select>

// 问题:如果只更新了order表,userMapper的二级缓存不会失效!
// 解决:使用flushCache="true"强制刷新
<select id="findUserWithOrders" resultMap="UserOrderMap" flushCache="true">
    SELECT ...
</select>

// 坑2:缓存与实体修改
public void cacheAndModify() {
    User user = mapper.findById(1L);  // 缓存到一级缓存
    
    user.setName("newName");  // 修改了user对象
    
    mapper.updateById(user);  // 更新数据库
    
    // 但是!一级缓存中的user对象也被修改了!
    // 如果后面又查询,返回的是修改后的对象
    User user2 = mapper.findById(1L);
    System.out.println(user2.getName());  // "newName"
}

四、动态SQL:MyBatis的精髓

4.1 动态SQL标签详解

<!-- 动态SQL核心标签 -->
<mapper namespace="com.example.mapper.UserMapper">

    <!-- WHERE条件动态拼接 -->
    <select id="searchUsers" resultType="User">
        SELECT * FROM user WHERE 1=1
        <if test="name != null and name != ''">
            AND name LIKE '%' || #{name} || '%'
        </if>
        <if test="email != null and email != ''">
            AND email = #{email}
        </if>
        <if test="status != null">
            AND status = #{status}
        </if>
        <if test="minAge != null">
            AND age >= #{minAge}
        </if>
        <if test="maxAge != null">
            AND age &lt;= #{maxAge}
        </if>
    </select>

    <!-- choose/when/otherwise(类似switch) -->
    <select id="searchByCondition" resultType="User">
        SELECT * FROM user
        <where>
            <choose>
                <when test="type == 'name'">
                    name = #{keyword}
                </when>
                <when test="type == 'email'">
                    email = #{keyword}
                </when>
                <otherwise>
                    id = #{defaultId}
                </otherwise>
            </choose>
        </where>
    </select>

    <!-- set标签(用于更新) -->
    <update id="updateUser">
        UPDATE user
        <set>
            <if test="name != null">name = #{name},</if>
            <if test="email != null">email = #{email},</if>
            <if test="status != null">status = #{status},</if>
        </set>
        WHERE id = #{id}
        <!-- 生成的SQL不会有多余的逗号 -->
    </update>

    <!-- foreach循环 -->
    <select id="findByIds" resultType="User">
        SELECT * FROM user
        WHERE id IN
        <foreach collection="ids" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
        <!-- 生成:WHERE id IN (1, 2, 3, 4, 5) -->
    </select>

    <!-- 批量插入 -->
    <insert id="batchInsert">
        INSERT INTO user(name, email) VALUES
        <foreach collection="users" item="user" separator=",">
            (#{user.name}, #{user.email})
        </foreach>
        <!-- 生成:INSERT INTO user(name, email) VALUES (?, ?), (?, ?), ... -->
    </insert>

    <!-- bind标签(绑定变量) -->
    <select id="searchByPattern" resultType="User">
        <bind name="pattern" value="'%' + namePattern + '%'"/>
        SELECT * FROM user WHERE name LIKE #{pattern}
    </select>

</mapper>

4.2 动态SQL原理:SqlNode

/**
 * 动态SQL解析原理
 * 
 * MyBatis使用SqlNode来表示SQL的各个部分:
 * - StaticTextSqlNode:静态文本
 * - IfSqlNode:if条件
 * - ForeachSqlNode:foreach循环
 * - WhereSqlNode:处理WHERE关键字
 * - SetSqlNode:处理SET关键字
 * - TrimSqlNode:自定义trim
 */

// 解析过程
public class SqlNodeParsing {
    
    // XML中的<if test="...">会被解析为IfSqlNode
    public class IfSqlNode implements SqlNode {
        private ExpressionEvaluator evaluator;
        private String test;  // test表达式
        private SqlNode contents;  // if body
        
        public boolean apply(DynamicContext context) {
            // 使用OGNL表达式评估test条件
            Boolean result = evaluator.evaluateBoolean(test, context.getBindings());
            if (result) {
                // 条件为true,应用子节点(拼接SQL片段)
                contents.apply(context);
                return true;
            }
            return false;
        }
    }
    
    // WhereSqlNode自动处理AND/OR前缀
    public class WhereSqlNode extends TrimSqlNode {
        public WhereSqlNode(SqlNode contents) {
            super(contents, "WHERE", null, "AND", "OR");
        }
        // 原理:去掉首个AND/OR,并添加WHERE关键字
    }
    
    // SetSqlNode自动处理SET逗号
    public class SetSqlNode extends TrimSqlNode {
        public SetSqlNode(SqlNode contents) {
            super(contents, "SET", ",", null, null);
        }
        // 原理:去掉末尾逗号,并添加SET关键字
    }
}

4.3 自定义SQL片段:include

<mapper namespace="com.example.mapper.UserMapper">

    <!-- 定义可复用的SQL片段 -->
    <sql id="userColumns">
        id, name, email, status, create_time
    </sql>
    
    <sql id="whereClause">
        <where>
            <if test="status != null">
                status = #{status}
            </if>
            <if test="keyword != null">
                AND (name LIKE #{keyword} OR email LIKE #{keyword})
            </if>
        </where>
    </sql>

    <!-- 使用SQL片段 -->
    <select id="findAll" resultType="User">
        SELECT <include refid="userColumns"/> FROM user
        <include refid="whereClause"/>
    </select>
    
    <!-- 支持属性覆盖 -->
    <select id="findByName" resultType="User">
        SELECT <include refid="userColumns">
            <property name="tableAlias" value="u"/>
        </include> 
        FROM user u
        WHERE u.name = #{name}
    </select>

</mapper>

五、插件机制:Interceptor

5.1 插件拦截点

┌─────────────────────────────────────────────────────────────────────┐
│                      MyBatis四大拦截点                               │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  1. Executor(方法级别)                                              │
│     - update, query, flushStatements, commit, rollback, getTransaction
│     - 拦截时机:SQL执行前/后                                          │
│     - 用途:分页插件、慢SQL日志                                       │
│                                                                      │
│  2. StatementHandler(SQL构建阶段)                                  │
│     - prepare, parameterize, batch, query, update                    │
│     - 拦截时机:SQL预编译前/后                                        │
│     - 用途:SQL改写(分页SQL改写)                                    │
│                                                                      │
│  3. ParameterHandler(参数处理阶段)                                 │
│     - getParameterObject, setParameters                              │
│     - 拦截时机:设置参数前/后                                         │
│     - 用途:参数加密、参数处理                                        │
│                                                                      │
│  4. ResultSetHandler(结果处理阶段)                                 │
│     - handleResultSets, handleOutputParameters                       │
│     - 拦截时机:结果集处理前/后                                       │
│     - 用途:结果解密、脱敏                                            │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

5.2 分页插件实战

// 1. 定义分页插件
@Intercepts({
    @Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
    )
})
public class PaginationInterceptor implements Interceptor {
    
    private static final String BOUND_SQL_KEY = "-boundSql";
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) 
            Plugin.unwrap(invocation.getTarget());
        
        // 1. 获取原始SQL
        String originalSql = statementHandler.getBoundSql().getSql();
        
        // 2. 从ThreadLocal获取分页参数
        Page<?> page = PageHelper.getLocalPage();
        if (page == null) {
            return invocation.proceed();  // 无分页参数,放行
        }
        
        // 3. 获取BoundSql对象
        BoundSql boundSql = statementHandler.getBoundSql();
        
        // 4. 拼接分页SQL(MySQL)
        String pageSql = originalSql + 
            " LIMIT " + (page.getPageNum() - 1) * page.getPageSize() + 
            ", " + page.getPageSize();
        
        // 5. 重新设置SQL(使用反射)
        Field field = BoundSql.class.getDeclaredField("sql");
        field.setAccessible(true);
        field.set(boundSql, pageSql);
        
        // 6. 继续执行
        return invocation.proceed();
    }
    
    @Override
    public Object plugin(Object target) {
        // 只拦截StatementHandler
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        }
        return target;
    }
    
    @Override
    public void setProperties(Properties properties) {
        // 从配置文件读取插件属性
    }
}

// 2. 配置插件
@Configuration
public class MyBatisConfig {
    
    @Bean
    public Interceptor[] paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        Properties properties = new Properties();
        properties.setProperty("helperDialect", "mysql");
        properties.setProperty("reasonable", "true");
        paginationInterceptor.setProperties(properties);
        return new Interceptor[]{paginationInterceptor};
    }
}

// 3. 使用分页
@Service
public class UserService {
    
    public PageInfo<User> findUserPage(int pageNum, int pageSize) {
        PageHelper.startPage(pageNum, pageSize);  // 设置分页参数
        List<User> users = userMapper.selectAll();
        return new PageInfo<>(users);  // 自动计算总数和分页信息
    }
}

5.3 通用Mapper插件

// 通用Mapper插件:为所有Mapper自动添加常用CRUD方法
@Intercepts({
    @Signature(type = Executor.class, method = "update", 
               args = {MappedStatement.class, Object.class})
})
public class AutoCRUDInterceptor implements Interceptor {
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        Object parameter = invocation.getArgs()[1];
        
        // 根据SQL类型自动处理
        switch (ms.getSqlCommandType()) {
            case INSERT:
                return handleInsert(ms, parameter);
            case UPDATE:
                return handleUpdate(ms, parameter);
            case DELETE:
                return handleDelete(ms, parameter);
            case SELECT:
                return invocation.proceed();
        }
        
        return invocation.proceed();
    }
    
    private int handleInsert(MappedStatement ms, Object parameter) {
        // 自动生成ID、创建时间、更新时间等
        if (parameter instanceof BaseEntity) {
            BaseEntity entity = (BaseEntity) parameter;
            if (entity.getId() == null) {
                entity.setId(SnowflakeIdGenerator.nextId());
            }
            if (entity.getCreateTime() == null) {
                entity.setCreateTime(new Date());
            }
        }
        return 0;  // 继续执行原SQL
    }
}

六、关联查询:ResultMap高级映射

6.1 一对一关联

<!-- 订单和用户:订单属于一个用户 -->
<resultMap id="OrderWithUserMap" type="Order">
    <!-- 主键映射 -->
    <id property="id" column="order_id"/>
    
    <!-- 普通字段映射 -->
    <result property="orderNo" column="order_no"/>
    <result property="amount" column="amount"/>
    
    <!-- 一对一关联:使用association -->
    <association property="user" javaType="User">
        <id property="id" column="user_id"/>
        <result property="name" column="user_name"/>
        <result property="email" column="user_email"/>
    </association>
</resultMap>

<select id="findOrderWithUser" resultMap="OrderWithUserMap">
    SELECT o.id as order_id, o.order_no, o.amount,
           u.id as user_id, u.name as user_name, u.email as user_email
    FROM orders o
    LEFT JOIN user u ON o.user_id = u.id
    WHERE o.id = #{orderId}
</select>

6.2 一对多关联

<!-- 用户和订单:一个用户有多个订单 -->
<resultMap id="UserWithOrdersMap" type="User">
    <id property="id" column="user_id"/>
    <result property="name" column="user_name"/>
    <result property="email" column="user_email"/>
    
    <!-- 一对多关联:使用collection -->
    <collection property="orders" ofType="Order">
        <id property="id" column="order_id"/>
        <result property="orderNo" column="order_no"/>
        <result property="amount" column="amount"/>
    </collection>
</resultMap>

<!-- N+1问题优化:使用JOIN -->
<select id="findUserWithOrders" resultMap="UserWithOrdersMap">
    SELECT u.id as user_id, u.name as user_name, u.email as user_email,
           o.id as order_id, o.order_no, o.amount
    FROM user u
    LEFT JOIN orders o ON u.id = o.user_id
    WHERE u.id = #{userId}
</select>

6.3 嵌套查询 vs 嵌套结果

┌─────────────────────────────────────────────────────────────────────┐
│                   关联查询两种方式对比                                │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  方式1:嵌套查询(Separate Query)                                   │
│  ┌────────────────────────────────────────────────────────────────┐ │
│  │  SELECT * FROM user WHERE id = 1                               │ │
│  │  ↓ 用户查询                                                     │ │
│  │  SELECT * FROM orders WHERE user_id = 1                         │ │
│  │  ↓ 订单查询(根据用户ID)                                       │ │
│  │  SELECT * FROM order_items WHERE order_id IN (1, 2, 3)          │ │
│  │  ↓ 订单项查询(根据订单ID列表)                                 │ │
│  └────────────────────────────────────────────────────────────────┘ │
│  问题:N+1查询!每个关联都会触发一次额外查询                          │
│  优点:可以复用单表查询                                              │
│  适用:关联表数据量大、不经常同时查询的场景                           │
│                                                                      │
│  方式2:嵌套结果(Nested Results)                                    │
│  ┌────────────────────────────────────────────────────────────────┐ │
│  │  SELECT u.*, o.* FROM user u LEFT JOIN orders o ON u.id = o.user_id │
│  │                          LEFT JOIN order_items i ON o.id = i.order_id│ │
│  └────────────────────────────────────────────────────────────────┘ │
│  优点:单次查询解决所有数据                                          │
│  问题:大数据量时SQL复杂、结果集大                                    │
│  适用:关联数据量可控、经常同时查询的场景                             │
│                                                                      │
│  推荐:优先使用嵌套结果,通过JOIN一次性获取                            │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

七、常见面试题

Q1:MyBatis为什么能防止SQL注入?

1. 使用PreparedStatement(预编译)
   - SQL结构在编译时确定
   - 参数以?占位符形式存在
   - 参数不会被当作SQL语句执行
   
2. 参数绑定使用#{}
   - #{name} → 使用PreparedStatement的setString()
   - 会进行类型转换和转义处理
   
3. 区别:#{} vs ${}
   - #{name}:预编译参数绑定,防SQL注入
   - ${name}:直接字符串替换,不防SQL注入
   - ${}使用场景:表名、列名动态确定时(需严格校验)

Q2:MyBatis一级缓存和二级缓存的区别?

┌─────────────────────────────────────────────────────────────────────┐
│                  缓存对比表                                         │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  ┌──────────────┬─────────────────┬─────────────────────────────────┐ │
│  │              │    一级缓存     │           二级缓存              │ │
│  ├──────────────┼─────────────────┼─────────────────────────────────┤ │
│  │  作用域       │  SqlSession     │  Mapper级别(全局)              │ │
│  │  存储位置     │  BaseExecutor   │  CachingExecutor                │ │
│  │  存储介质     │  PerpetualCache │  可配置(内存/Redis/Ehcache)   │ │
│  │  生命周期     │  SqlSession周期 │  应用运行期间                   │ │
│  │  默认开启     │  是             │  否(需配置<cache>)            │ │
│  │  失效时机     │  commit/rollbackcommit后生效                  │ │
│  └──────────────┴─────────────────┴─────────────────────────────────┘ │
│                                                                      │
│  执行顺序:二级缓存 → 一级缓存 → 数据库                               │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Q3:MyBatis如何获取自增ID?

<!-- 方式1:useGeneratedKeys + keyProperty -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO user(name, email) VALUES(#{name}, #{email})
</insert>

<!-- 方式2:selectKey子查询 -->
<insert id="insert">
    <selectKey keyProperty="id" resultType="long" order="BEFORE">
        SELECT IFNULL(MAX(id), 0) + 1 FROM user
    </selectKey>
    INSERT INTO user(id, name, email) VALUES(#{id}, #{name}, #{email})
</insert>

Q4:MyBatis延迟加载的原理?

原理:使用CGLIB或Javassist生成代理对象

┌─────────────────────────────────────────────────────────────────────┐
│                    延迟加载执行流程                                 │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  1. 查询主对象(不查询关联对象)                                      │
│     SELECT * FROM user WHERE id = 1                                 │
│                                                                      │
│  2. 返回代理对象(UserMapperProxy)                                  │
│     User user = userMapper.findById(1);                             │
│     // user对象实际是代理对象                                        │
│                                                                      │
│  3. 访问关联属性时触发加载                                           │
│     System.out.println(user.getOrders());                           │
│     // 代理对象拦截getOrders(),执行关联查询                          │
│     SELECT * FROM orders WHERE user_id = 1                           │
│                                                                      │
│  配置:                                                              │
│  <settings>                                                          │
│      <setting name="lazyLoadingEnabled" value="true"/>               │
│      <setting name="aggressiveLazyLoading" value="false"/>          │
│  </settings>                                                        │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

八、性能优化实践

8.1 SQL优化建议

<!-- 1. 使用分页查询,避免SELECT * -->
<select id="findUserPage" resultType="User">
    SELECT id, name, email, status 
    FROM user 
    WHERE status = 1 
    LIMIT #{offset}, #{pageSize}
</select>

<!-- 2. 批量操作,减少数据库交互 -->
<insert id="batchInsert" parameterType="java.util.List">
    INSERT INTO user(name, email) VALUES
    <foreach collection="list" item="item" separator=",">
        (#{item.name}, #{item.email})
    </foreach>
    <!-- 一次SQL插入1000条,减少网络开销 -->
</insert>

<!-- 3. 使用JOIN替代子查询(MySQL优化器优化) -->
<!-- 不推荐:子查询 -->
<select id="findUserWithOrders" resultType="User">
    SELECT * FROM user 
    WHERE id IN (SELECT user_id FROM orders WHERE amount > 1000)
</select>

<!-- 推荐:JOIN -->
<select id="findUserWithOrders" resultType="User">
    SELECT DISTINCT u.* 
    FROM user u 
    INNER JOIN orders o ON u.id = o.user_id 
    WHERE o.amount > 1000
</select>

<!-- 4. 利用覆盖索引,避免回表 -->
<!-- 假设:index(name, email, status) -->
<select id="findByName" resultType="User">
    SELECT name, email FROM user WHERE name = #{name}
    <!-- 无需回表,直接在索引中获取数据 -->
</select>

8.2 配置优化

# MyBatis配置优化
mybatis:
  configuration:
    # 开启驼峰命名转换(数据库下划线 → Java驼峰)
    map-underscore-to-camel-case: true
    
    # 开启延迟加载
    lazy-loading-enabled: true
    aggressive-lazy-loading: false
    
    # 开启二级缓存
    cache-enabled: true
    
    # 打印SQL日志(开发环境)
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    
    # 全局超时时间
    default-statement-timeout: 30
    
    # JDBC类型处理(空值处理)
    jdbc-type-for-null: 'null'
    
  mapper-locations: classpath:mapper/**/*.xml
  type-aliases-package: com.example.entity
  type-handlers-package: com.example.handler

九、总结

┌─────────────────────────────────────────────────────────────────────┐
│                      MyBatis知识地图                                 │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  核心组件 ──────────────────────────────────────────────────────→   │
│  SqlSessionExecutorStatementHandler                           │
│              → ParameterHandlerResultSetHandler                  │
│                                                                      │
│  缓存机制 ──────────────────────────────────────────────────────→   │
│  一级缓存(SqlSession) → 二级缓存(Mapper)                             │
│                                                                      │
│  动态SQL ──────────────────────────────────────────────────────→   │
│  if/choose/when/otherwise → where/set/trim → foreach/bind          │
│                                                                      │
│  插件机制 ──────────────────────────────────────────────────────→   │
│  Interceptor → @Signature → Executor/StatementHandler/...          │
│                                                                      │
│  关联映射 ──────────────────────────────────────────────────────→   │
│  association(一对一) → collection(一对多)                            │
│                                                                      │
│  性能优化 ──────────────────────────────────────────────────────→   │
│  分页查询 → 批量操作 → 覆盖索引 → 延迟加载                           │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

🎯 讨论话题

大家在使用MyBatis时遇到过哪些"奇葩"问题?是怎么排查和解决的?


往期热门文章推荐:


如果这篇文章对你有帮助,欢迎点赞、收藏、转发!我们下期再见! 👋