Skip to content

MyBatis

一、为什么需要 MyBatis?

原生 JDBC 操作数据库需要手写大量样板代码:加载驱动、创建连接、拼接 SQL、处理 ResultSet、关闭资源。SQL 硬编码在 Java 中,修改 SQL 需要重新编译。

MyBatis 将 SQL 从 Java 代码中分离到 XML 或注解中,自动处理参数绑定和结果映射,让你专注于 SQL 本身。相比于 JPA/Hibernate 的全自动 ORM,MyBatis 是半自动——你完全掌控 SQL,适合复杂查询和性能优化场景。

二、核心架构

┌─────────────────────────────────────────┐
│               MyBatis 核心               │
├─────────────────────────────────────────┤
│  SqlSessionFactory(全局唯一,线程安全)  │
│         │                                │
│         ▼                                │
│  SqlSession(一次会话,非线程安全)       │
│         │                                │
│         ▼                                │
│  Executor(执行器)                       │
│  ├── SimpleExecutor(默认)              │
│  ├── ReuseExecutor(复用 Statement)      │
│  └── BatchExecutor(批量操作)            │
│         │                                │
│    ┌────┴────┐                           │
│    ▼         ▼                           │
│  MappedStatement  Configuration          │
│  (封装 SQL)  (全局配置)               │
└─────────────────────────────────────────┘

执行流程:

  1. 读取配置文件,创建 SqlSessionFactory
  2. SqlSessionFactory 获取 SqlSession
  3. SqlSession 调用 Executor 执行 SQL
  4. ExecutorMappedStatement 获取 SQL 和参数映射
  5. 通过 TypeHandler 处理参数和结果
  6. 返回映射后的 Java 对象

三、核心配置

3.1 XML 映射

xml
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
    <select id="findById" resultType="com.example.entity.User">
        SELECT id, name, email, created_at
        FROM users
        WHERE id = #{id}
    </select>
</mapper>
java
// UserMapper.java
public interface UserMapper {
    User findById(Long id);
}

3.2 #{} vs ${}

#{}${}
处理方式预编译占位符 ?字符串替换
SQL 注入安全危险
适用场景参数值表名、列名、ORDER BY
xml
<!-- #{}:预编译,安全 -->
SELECT * FROM users WHERE id = #{id}
→ SELECT * FROM users WHERE id = ?

<!-- ${}:字符串替换,防 SQL 注入 -->
SELECT * FROM ${tableName} WHERE id = #{id}  <!-- tableName 不能传用户输入 -->

为什么 #{} 能防止 SQL 注入? 预编译时 SQL 结构已确定,? 占位符只接受参数值,恶意 SQL 片段被视为字符串而非 SQL 指令。

3.3 动态 SQL

xml
<select id="findByCondition" resultType="User">
    SELECT * FROM users
    <where>
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
        <if test="email != null">
            AND email = #{email}
        </if>
        <if test="status != null">
            AND status = #{status}
        </if>
    </where>
</select>
标签作用
<where>自动去掉第一个 AND/OR
<if>条件判断
<choose>/<when>/<otherwise>多选一(类似 switch)
<foreach>遍历集合(IN 查询)
<set>UPDATE 时自动去掉多余逗号
<trim>自定义截取

四、缓存机制

4.1 一级缓存(SqlSession 级别)

原理: 同一个 SqlSession 内,相同查询只执行一次 SQL,后续从缓存取。默认开启,无法关闭。

java
SqlSession session = factory.openSession();
User u1 = session.selectOne("findById", 1L);  // 查数据库
User u2 = session.selectOne("findById", 1L);  // 从缓存取,u1 == u2 为 true
session.close();

一级缓存失效场景: 执行了 insert/update/delete(清空缓存)、手动 clearCache()、不同 SqlSession。

4.2 二级缓存(Mapper 级别)

原理: 跨 SqlSession 共享,基于 namespace。需显式开启:

xml
<!-- 1. 全局配置 -->
<setting name="cacheEnabled" value="true"/>

<!-- 2. Mapper 中声明 -->
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>

注意: 二级缓存存的是序列化后的数据,查询时反序列化返回新对象,不是同一个引用。多表关联查询缓存失效问题严重,生产环境谨慎使用。

五、插件机制

原理: MyBatis 通过责任链模式拦截四大对象的方法调用:

拦截对象可拦截方法
Executorupdate, query, commit, rollback
StatementHandlerprepare, parameterize, batch, update, query
ParameterHandlergetParameterObject, setParameters
ResultSetHandlerhandleResultSets, handleOutputParameters
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 {
        // 修改 SQL,添加 LIMIT 子句
        // 执行修改后的 SQL
        // 返回分页结果
        return invocation.proceed();
    }
}

六、与 Spring Boot 集成

yaml
mybatis:
  mapper-locations: classpath:mapper/*.xml      # XML 映射文件位置
  type-aliases-package: com.example.entity       # 实体类包
  configuration:
    map-underscore-to-camel-case: true           # 下划线转驼峰
    log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl

七、面试要点

Q:MyBatis 和 JPA/Hibernate 的区别?

MyBatis 是半自动 ORM,SQL 由开发者完全控制,灵活但工作量大;Hibernate 是全自动 ORM,自动生成 SQL,开发快但复杂查询优化困难。

Q:#{}${} 的区别?

#{} 是预编译占位符,安全防注入;${} 是字符串替换,有注入风险,仅用于动态表名/列名。

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

一级缓存 SqlSession 级别,默认开启,同一 Session 内相同查询只查一次;二级缓存 Mapper 级别,跨 Session 共享,需显式开启,序列化存储。

Q:MyBatis 插件原理?

基于动态代理和责任链模式,拦截 Executor、StatementHandler、ParameterHandler、ResultSetHandler 四大对象的方法。分页插件(PageHelper)就是通过拦截 Executor.query() 改写 SQL 实现。