MyBatis 专题深度细化解析

一、核心概念与架构设计

1. MyBatis 核心定义与定位

问题:什么是 MyBatis?它的核心价值是什么?

  • 本质定义 :MyBatis 是一个半自动 ORM(Object-Relational Mapping)持久层框架

  • 核心功能

    • 自定义 SQL:开发者完全控制 SQL 编写

    • 存储过程支持:可调用数据库存储过程

    • 高级映射:复杂对象关系映射支持

  • 设计哲学:在 SQL 可控性和开发效率之间取得平衡

2. MyBatis 与全自动 ORM 的对比分析

问题:为什么说 MyBatis 是半自动 ORM?与 Hibernate 的根本区别?

特性维度 MyBatis (半自动) Hibernate (全自动)
SQL 控制 开发者手动编写和优化 框架自动生成,开发者无需关心
关联查询 需手动编写 JOIN 或嵌套查询 SQL 通过对象导航自动加载
性能调优 直接优化 SQL 语句 优化 HQL、缓存配置等
学习曲线 较低,贴近传统 JDBC 较高,需要理解 Session、持久化状态等概念
适用场景 复杂查询、高性能要求、遗留系统 标准 CRUD、快速开发、数据库无关性要求高

3. MyBatis 核心价值主张

问题:MyBatis 的主要优势有哪些?

java 复制代码
// 优势体现示例
public class MyBatisAdvantages {
    // 1. SQL 与代码分离
    // SQL 在 XML/注解中,业务代码更清晰
    @Select("SELECT * FROM users WHERE status = #{status}")
    List<User> findActiveUsers(@Param("status") String status);
    
    // 2. 精准的 SQL 控制
    // 可以编写复杂查询,利用数据库特定功能
    public void complexQuery() {
        // 可以使用窗口函数、CTE 等高级 SQL 特性
    }
    
    // 3. 简化 JDBC 样板代码
    // 自动处理连接、参数设置、结果集映射
}

二、SQL 处理机制深度解析

4. #{} 与 ${} 的底层原理剖析

问题:详细说明 #{} 和 ${} 的工作原理和区别

#{} (预编译占位符)

处理流程:

  1. 解析阶段 :MyBatis 解析 SQL,将 #{param} 替换为 ?

  2. 参数设置 :通过 PreparedStatement.setXxx() 方法安全设置参数

  3. SQL 执行:数据库接收预编译的 SQL 和参数值

安全机制:

sql 复制代码
-- 原始 SQL
SELECT * FROM users WHERE name = #{username}

-- 转换后 SQL (发送到数据库)
SELECT * FROM users WHERE name = ?

-- 参数值单独传递 (防止 SQL 注入)
PreparedStatement.setString(1, "John'; DROP TABLE users; --")
${} (字符串替换)

处理流程:

  1. 直接替换 :MyBatis 将 ${param} 直接替换为参数值的字符串形式

  2. SQL 拼接:生成完整的 SQL 语句发送到数据库

风险示例:

sql 复制代码
-- 原始 SQL (危险!)
SELECT * FROM ${tableName} WHERE id = #{id}

-- 如果 tableName = "users; DROP TABLE products; --"
-- 最终 SQL
SELECT * FROM users; DROP TABLE products; -- WHERE id = ?
使用场景对比
场景 推荐方式 理由
WHERE 条件值 #{} 防止 SQL 注入
表名/列名 ${} 动态指定数据库对象
ORDER BY 子句 ${} 动态排序字段
LIKE 查询 #{} + Java 拼接 安全且灵活

5. 动态 SQL 的完整技术栈

问题:MyBatis 动态 SQL 的完整标签集和执行原理

9 大动态 SQL 标签详解

1. <if> 条件判断

XML 复制代码
<select id="findUsers" parameterType="map" resultType="User">
  SELECT * FROM users
  WHERE 1=1
  <if test="name != null and name != ''">
    AND name = #{name}
  </if>
  <if test="age != null">
    AND age = #{age}
  </if>
</select>

2. <choose><when><otherwise> 多路选择

XML 复制代码
<select id="findUsers" resultType="User">
  SELECT * FROM users
  WHERE
  <choose>
    <when test="name != null">
      name = #{name}
    </when>
    <when test="email != null">
      email = #{email}
    </when>
    <otherwise>
      status = 'ACTIVE'
    </otherwise>
  </choose>
</select>

3. <where> 智能 WHERE 子句

XML 复制代码
<select id="findUsers" parameterType="map" resultType="User">
  SELECT * FROM users
  <where>
    <if test="name != null">name = #{name}</if>
    <if test="email != null">AND email = #{email}</if>
  </where>
</select>
<!-- 自动处理 AND 前缀,如果所有条件为空则省略 WHERE -->

4. <set> 智能 UPDATE 语句

XML 复制代码
<update id="updateUser" parameterType="User">
  UPDATE users
  <set>
    <if test="name != null">name = #{name},</if>
    <if test="email != null">email = #{email},</if>
    <if test="age != null">age = #{age},</if>
  </set>
  WHERE id = #{id}
</update>
<!-- 自动处理尾部逗号 -->

5. <trim> 自定义修剪

XML 复制代码
<trim prefix="WHERE" prefixOverrides="AND |OR ">
  <if test="name != null">AND name = #{name}</if>
</trim>
<!-- 更灵活的字符串修剪 -->

6. <foreach> 循环遍历

XML 复制代码
<select id="findUsersByIds" parameterType="list" resultType="User">
  SELECT * FROM users
  WHERE id IN
  <foreach item="id" collection="list" open="(" separator="," close=")">
    #{id}
  </foreach>
</select>

7. <bind> 变量绑定

XML 复制代码
<select id="findUsers" parameterType="map" resultType="User">
  <bind name="pattern" value="'%' + name + '%'" />
  SELECT * FROM users
  WHERE name LIKE #{pattern}
</select>
OGNL 表达式引擎

执行原理:

java 复制代码
// OGNL (Object-Graph Navigation Language) 示例
public class OGNLExample {
    public void processDynamicSQL() {
        // MyBatis 使用 OGNL 计算表达式的值
        // test="name != null and name != ''"
        
        // 支持的操作:
        // - 属性访问: user.name
        // - 方法调用: user.getName()
        // - 数学运算: age > 18
        // - 逻辑运算: name != null and age > 18
        // - 集合操作: list != null and list.size() > 0
    }
}

三、缓存机制深度剖析

6. 一级缓存详细工作机制

问题:一级缓存的实现原理和生命周期

作用域与生命周期
java 复制代码
public class FirstLevelCacheDemo {
    public void cacheBehavior() {
        SqlSession sqlSession = sqlSessionFactory.openSession();
        try {
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            
            // 第一次查询 - 访问数据库
            User user1 = mapper.selectUser(1L);
            
            // 第二次查询相同数据 - 从一级缓存获取
            User user2 = mapper.selectUser(1L);
            // user1 == user2 (同一个对象引用)
            
            // 任何 INSERT/UPDATE/DELETE 操作都会清空一级缓存
            mapper.updateUser(user1);
            
            // 第三次查询 - 再次访问数据库(缓存已清空)
            User user3 = mapper.selectUser(1L);
            
        } finally {
            sqlSession.close(); // 关闭 Session,缓存销毁
        }
    }
}
缓存Key生成算法
java 复制代码
public class CacheKeyGenerator {
    // MyBatis 生成缓存Key的要素:
    // 1. MappedStatement的id
    // 2. 分页参数 (RowBounds)  
    // 3. 查询SQL本身
    // 4. 参数值
    // 5. 环境id (environmentId)
    
    public CacheKey createCacheKey() {
        CacheKey cacheKey = new CacheKey();
        cacheKey.update(mappedStatementId);
        cacheKey.update(rowBounds.getOffset());
        cacheKey.update(rowBounds.getLimit());
        cacheKey.update(boundSql.getSql());
        // ... 参数处理
        return cacheKey;
    }
}

7. 二级缓存架构设计

问题:二级缓存的配置、序列化要求和作用域

配置与启用
XML 复制代码
<!-- 1. 全局启用二级缓存 -->
<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

<!-- 2. Mapper XML 中配置缓存 -->
<mapper namespace="com.example.UserMapper">
    <cache
        eviction="LRU"
        flushInterval="60000"
        size="512"
        readOnly="true"/>
    
    <!-- 3. 实体类必须实现 Serializable -->
    public class User implements Serializable {
        private static final long serialVersionUID = 1L;
        // ... fields
    }
</mapper>
缓存策略对比
策略 描述 适用场景
LRU (默认) 最近最少使用 热点数据集中
FIFO 先进先出 数据访问均匀
SOFT 软引用 内存敏感场景
WEAK 弱引用 临时缓存

四、插件机制深度解析

8. 插件拦截原理与实现

问题:MyBatis 插件的拦截机制和实现步骤

四大可拦截组件
java 复制代码
public interface PluginTargets {
    // 1. Executor - SQL执行器
    // 拦截方法: update, query, commit, rollback等
    
    // 2. StatementHandler - SQL语法构建
    // 拦截方法: prepare, parameterize等
    
    // 3. ParameterHandler - 参数处理
    // 拦截方法: setParameters
    
    // 4. ResultSetHandler - 结果集处理  
    // 拦截方法: handleResultSets, handleOutputParameters
}
插件实现完整示例
java 复制代码
@Intercepts({
    @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
    )
})
public class QueryStatsPlugin implements Interceptor {
    private static final Logger logger = LoggerFactory.getLogger(QueryStatsPlugin.class);
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long startTime = System.currentTimeMillis();
        
        try {
            // 执行原始方法
            Object result = invocation.proceed();
            return result;
        } finally {
            long endTime = System.currentTimeMillis();
            long cost = endTime - startTime;
            
            MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
            logger.info("SQL执行统计 - ID: {}, 耗时: {}ms", ms.getId(), cost);
            
            // 慢SQL监控
            if (cost > 1000) {
                logger.warn("慢SQL警告: {}, 耗时: {}ms", ms.getId(), cost);
            }
        }
    }
    
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    
    @Override
    public void setProperties(Properties properties) {
        // 接收配置参数
    }
}
配置启用插件
XML 复制代码
<plugins>
    <plugin interceptor="com.example.QueryStatsPlugin">
        <property name="slowQueryThreshold" value="1000"/>
    </plugin>
</plugins>

五、关联映射深度技术

9. 一对一关联的两种实现模式

问题:详细说明 MyBatis 一对一关联的两种实现方式及其优劣

方式一:联合查询 (JOIN)
XML 复制代码
<!-- 单次SQL查询,性能较好 -->
<resultMap id="UserDetailMap" type="User">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <!-- 关联对象映射 -->
    <association property="profile" javaType="UserProfile">
        <id property="id" column="profile_id"/>
        <result property="realName" column="real_name"/>
        <result property="address" column="address"/>
    </association>
</resultMap>

<select id="selectUserWithProfile" resultMap="UserDetailMap">
    SELECT 
        u.id, u.username,
        p.id as profile_id, p.real_name, p.address
    FROM users u
    LEFT JOIN user_profiles p ON u.profile_id = p.id
    WHERE u.id = #{id}
</select>
方式二:嵌套查询 (N+1查询)
XML 复制代码
<!-- 分两次查询,更灵活但可能性能较差 -->
<resultMap id="UserWithProfileMap" type="User">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <!-- 嵌套查询 -->
    <association 
        property="profile" 
        column="profile_id"
        javaType="UserProfile"
        select="selectUserProfileById"/>
</resultMap>

<select id="selectUser" resultMap="UserWithProfileMap">
    SELECT id, username, profile_id FROM users WHERE id = #{id}
</select>

<select id="selectUserProfileById" resultType="UserProfile">
    SELECT id, real_name, address FROM user_profiles WHERE id = #{profile_id}
</select>
两种方式对比
维度 联合查询 嵌套查询
性能 一次查询,性能较好 N+1 查询问题,性能可能较差
灵活性 SQL 相对复杂 查询可复用,逻辑清晰
延迟加载 不支持 支持延迟加载
适用场景 关联数据量小,关系简单 关联复杂,需要延迟加载

10. 延迟加载的底层实现

问题:MyBatis 延迟加载的实现原理和技术细节

代理对象生成机制
java 复制代码
public class LazyLoadingDemo {
    public void lazyLoadBehavior() {
        User user = userMapper.selectUser(1L);
        // 此时 user.profile 是代理对象,不是 null
        
        // 第一次访问关联对象时触发查询
        String address = user.getProfile().getAddress();
        // 实际执行: SELECT * FROM user_profiles WHERE id = ?
    }
}
配置与实现原理
XML 复制代码
<!-- 启用延迟加载 -->
<settings>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>

CGLIB 代理实现:

java 复制代码
public class LazyLoader implements MethodInterceptor {
    private String sql;
    private Object parameter;
    private SqlSession sqlSession;
    
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        // 第一次调用方法时加载真实数据
        if (!loaded) {
            loadRealObject();
        }
        return method.invoke(realObject, args);
    }
    
    private void loadRealObject() {
        realObject = sqlSession.selectOne(sql, parameter);
        loaded = true;
    }
}

六、执行器与批处理

11. 三种执行器深度对比

问题:详细说明 MyBatis 三种执行器的区别和适用场景

SimpleExecutor
java 复制代码
public class SimpleExecutorBehavior {
    public void demo() {
        // 每次执行都创建新的 Statement
        User user1 = mapper.selectUser(1L); // 创建 Statement1
        User user2 = mapper.selectUser(2L); // 创建 Statement2
        // Statement1.close(), Statement2.close()
    }
}
// 特点:简单安全,但性能一般
ReuseExecutor
java 复制代码
public class ReuseExecutorBehavior {
    public void demo() {
        // 复用相同 SQL 的 Statement
        User user1 = mapper.selectUser(1L); // 创建 Statement (key: "selectUser")
        User user2 = mapper.selectUser(2L); // 复用 Statement
        User user3 = mapper.selectByName("John"); // 创建新 Statement (key: "selectByName")
        // Session 关闭时统一关闭所有 Statement
    }
}
// 特点:减少 Statement 创建开销
BatchExecutor
java 复制代码
public class BatchExecutorBehavior {
    public void demo() {
        // 批量操作
        for (int i = 0; i < 1000; i++) {
            User user = new User("user" + i);
            mapper.insertUser(user); // 添加到批处理
        }
        // 一次性执行所有 INSERT
        List<BatchResult> results = sqlSession.flushStatements();
    }
}
// 特点:批量操作,大幅提升性能
执行器选择策略
场景 推荐执行器 理由
Web 请求 SimpleExecutor 避免线程安全问题
报表生成 ReuseExecutor SQL 重复度高
数据导入 BatchExecutor 批量操作需求
默认选择 SimpleExecutor 平衡安全与性能

12. 批量处理最佳实践

问题:MyBatis 批量插入的实现方式和性能优化

方式一:BatchExecutor
java 复制代码
public class BatchInsertDemo {
    public void batchInsert(List<User> users) {
        SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
        try {
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            
            for (int i = 0; i < users.size(); i++) {
                mapper.insertUser(users.get(i));
                
                // 分批提交,避免内存溢出
                if (i % 1000 == 0) {
                    sqlSession.flushStatements();
                }
            }
            
            sqlSession.commit(); // 提交剩余数据
        } finally {
            sqlSession.close();
        }
    }
}
方式二:foreach 批量插入
XML 复制代码
<insert id="batchInsertUsers" parameterType="list">
    INSERT INTO users (name, email, age) VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.name}, #{user.email}, #{user.age})
    </foreach>
</insert>

性能对比:

  • BatchExecutor:适合大数据量,内存控制好

  • foreach:适合中小数据量,代码简单

  • JDBC 批量:最大性能,但需要原生 JDBC

七、高级特性与最佳实践

13. 类型处理器深度应用

问题:MyBatis 类型处理器的实现原理和自定义方法

枚举类型处理
java 复制代码
// 1. 定义枚举
public enum UserStatus {
    ACTIVE(1), INACTIVE(0), DELETED(-1);
    
    private final int code;
    UserStatus(int code) { this.code = code; }
    public int getCode() { return code; }
}

// 2. 自定义类型处理器
@MappedTypes(UserStatus.class)
@MappedJdbcTypes(JdbcType.INTEGER)
public class UserStatusTypeHandler extends BaseTypeHandler<UserStatus> {
    
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, 
                                  UserStatus parameter, JdbcType jdbcType) throws SQLException {
        ps.setInt(i, parameter.getCode()); // Java -> JDBC
    }
    
    @Override
    public UserStatus getNullableResult(ResultSet rs, String columnName) throws SQLException {
        int code = rs.getInt(columnName);
        return Arrays.stream(UserStatus.values())
                    .filter(status -> status.getCode() == code)
                    .findFirst()
                    .orElse(null); // JDBC -> Java
    }
    
    // 其他重载方法...
}
JSON 类型处理
java 复制代码
public class JsonTypeHandler<T> extends BaseTypeHandler<T> {
    private final Class<T> type;
    private final ObjectMapper objectMapper = new ObjectMapper();
    
    public JsonTypeHandler(Class<T> type) {
        this.type = type;
    }
    
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, 
                                  T parameter, JdbcType jdbcType) throws SQLException {
        try {
            ps.setString(i, objectMapper.writeValueAsString(parameter));
        } catch (JsonProcessingException e) {
            throw new SQLException("JSON序列化失败", e);
        }
    }
    
    @Override
    public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String json = rs.getString(columnName);
        if (json != null) {
            try {
                return objectMapper.readValue(json, type);
            } catch (IOException e) {
                throw new SQLException("JSON反序列化失败", e);
            }
        }
        return null;
    }
}

14. 多参数传递的完整方案

问题:MyBatis 中传递多个参数的各种方法及其适用场景

方案一:@Param 注解(推荐)
java 复制代码
public interface UserMapper {
    List<User> findUsersByMultipleParams(
        @Param("name") String name,
        @Param("minAge") Integer minAge,
        @Param("status") UserStatus status
    );
}
XML 复制代码
<select id="findUsersByMultipleParams" resultType="User">
    SELECT * FROM users
    WHERE 1=1
    <if test="name != null">AND name = #{name}</if>
    <if test="minAge != null">AND age >= #{minAge}</if>
    <if test="status != null">AND status = #{status}</if>
</select>
方案二:Map 参数
java 复制代码
public interface UserMapper {
    List<User> findUsersByMap(Map<String, Object> params);
}
XML 复制代码
<select id="findUsersByMap" parameterType="map" resultType="User">
    SELECT * FROM users
    WHERE name = #{name} AND age = #{age}
</select>
方案三:POJO 参数对象
java 复制代码
public class UserQuery {
    private String name;
    private Integer minAge;
    private Integer maxAge;
    private UserStatus status;
    // getters/setters
}
XML 复制代码
<select id="findUsersByQuery" parameterType="UserQuery" resultType="User">
    SELECT * FROM users
    WHERE 1=1
    <if test="name != null">AND name = #{name}</if>
    <if test="minAge != null">AND age >= #{minAge}</if>
    <if test="maxAge != null">AND age <= #{maxAge}</if>
    <if test="status != null">AND status = #{status}</if>
</select>
方案对比总结
方案 优点 缺点 适用场景
@Param 类型安全,IDE 支持好 参数较多时代码冗长 2-5个参数
Map 灵活,易于扩展 类型不安全,IDE 支持差 动态参数
POJO 类型安全,易于维护 需要创建额外类 复杂查询条件
相关推荐
绝无仅有2 小时前
Redis 面试题解析:某度互联网大厂
后端·面试·架构
绝无仅有2 小时前
某度互联网大厂 MySQL 面试题解析
后端·面试·架构
无心水2 小时前
【中间件:Redis】3、Redis数据安全机制:持久化(RDB+AOF)+事务+原子性(面试3大考点)
redis·中间件·面试·后端面试·redis事务·redis持久化·redis原子性
JH30733 小时前
MyBatis多表联查返回List仅一条数据?主键冲突BUG排查与解决
bug·mybatis
乄夜3 小时前
嵌入式面试高频!!!C语言(十四) STL(嵌入式八股文)
c语言·c++·stm32·单片机·mcu·面试·51单片机
九年义务漏网鲨鱼4 小时前
【机器学习算法】面试中的ROC和AUC
算法·机器学习·面试
白露与泡影6 小时前
面试:Spring中单例模式用的是哪种?
spring·单例模式·面试
小坏讲微服务12 小时前
Spring Boot整合Redis注解,实战Redis注解使用
spring boot·redis·分布式·后端·spring cloud·微服务·mybatis
CodeLongBear12 小时前
MySQL索引篇 -- 从数据页的角度看B+树
mysql·面试