Skip to content

MyBatis 这套东西我平时一直在用,但一直没有系统地把它从头到尾梳理一遍。这篇把自己散落的知识点整理成一条主线:它解决了 JDBC 的什么痛点、核心组件是什么、映射和动态 SQL 怎么写、缓存怎么用、最后怎么和 Spring Boot 接上。

一、为什么需要 MyBatis

先看一段最原始的 JDBC 代码,就知道 MyBatis 解决的是什么:

java
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
    conn = DriverManager.getConnection(url, user, password);
    ps = conn.prepareStatement("select * from user where id = ?");
    ps.setInt(1, 1);
    rs = ps.executeQuery();
    while (rs.next()) {
        User u = new User();
        u.setId(rs.getInt("id"));        // 手动逐字段映射
        u.setName(rs.getString("name"));
        // ...
    }
} finally {
    if (rs != null) rs.close();          // 手动关资源
    if (ps != null) ps.close();
    if (conn != null) conn.close();
}

四个痛点,每个都很烦:

痛点说明
样板代码多连接、Statement、ResultSet 遍历、关闭,每个查询都要写一遍
SQL 硬编码SQL 写在 Java 字符串里,改 SQL 要改代码重新编译
手动映射从 ResultSet 逐字段 set 到对象,字段多了就是灾难
参数拼接易注入字符串拼 SQL,稍不注意就有注入风险

MyBatis 的定位是半自动 ORMSQL 还是你自己写(保留 SQL 的灵活性和可控性),但参数绑定、结果映射、连接管理这些脏活累活它全包了。对比全自动的 Hibernate/JPA(SQL 由框架生成),MyBatis 更贴近 DBA 和复杂 SQL 场景。

二、核心组件

MyBatis 的骨架就一条链,从配置到执行:

SqlSessionFactoryBuilder  →  SqlSessionFactory  →  SqlSession  →  Mapper
(读配置,一次性)          (工厂,全局一个)    (会话,一次交互)  (你写的接口)
组件作用生命周期
SqlSessionFactoryBuilder读 mybatis-config.xml 构建工厂用完即弃
SqlSessionFactory生产 SqlSession应用级单例
SqlSession执行 SQL、拿 Mapper、事务方法级,用完关闭
Mapper 接口你定义的 UserMapper,MyBatis 动态代理实现与 SqlSession 绑定

一条最简的查询流程:

java
// 1. 构建工厂
SqlSessionFactory factory = new SqlSessionFactoryBuilder()
    .build(Resources.getResourceAsStream("mybatis-config.xml"));

// 2. 开会话
try (SqlSession session = factory.openSession()) {
    // 3. 拿 Mapper(动态代理)
    UserMapper mapper = session.getMapper(UserMapper.class);
    // 4. 调方法,底层执行 SQL + 自动映射
    User user = mapper.selectById(1);
}

关键理解UserMapper 只是个接口,没有实现类。MyBatis 在运行时用 JDK 动态代理帮你生成实现——把接口方法名映射到 XML 里的 SQL id,执行后把结果自动映射成对象返回。

三、Mapper 映射:XML vs 注解

Mapper 接口的 SQL 有两种写法,各有利弊:

XML 方式(复杂 SQL 首选,动态 SQL 只能在 XML 里写):

xml
<!-- UserMapper.xml -->
<mapper namespace="com.demo.mapper.UserMapper">
    <select id="selectById" resultType="com.demo.entity.User">
        select * from user where id = #{id}
    </select>
</mapper>

注解方式(简单 SQL 省事):

java
public interface UserMapper {
    @Select("select * from user where id = #{id}")
    User selectById(int id);
}

#{} vs ${} 是最容易踩的坑,必须分清:

写法底层是否安全用途
#{id}预编译占位符 ?,参数绑定安全(防注入)值:where id = #{id}
${id}字符串直接拼接有注入风险表名/列名等不能预编译的:order by ${col}

一句话:#{} 就不要 ${}${} 只在表名、列名、动态排序这种"结构"场景才用。

resultMap 什么时候用:当表列名和 Java 字段名不一致时(比如表里是 user_name,实体是 userName),resultType 映射不上,就要用 resultMap 显式声明映射关系,或者靠 mapUnderscoreToCamelCase 开启下划线转驼峰(Spring Boot 里一行配置)。

四、动态 SQL

动态 SQL 是 MyBatis 最实用的能力——按条件拼 SQL,不用自己写一堆 if 字符串拼接。几个常用标签:

xml
<select id="selectByCondition" resultType="User">
    select * from user
    <where>                          <!-- where 标签:自动处理第一个 and/or -->
        <if test="name != null">
            and name = #{name}
        </if>
        <if test="age != null">
            and age >= #{age}
        </if>
    </where>
</select>

<!-- 批量:foreach -->
<select id="selectByIds" resultType="User">
    select * from user where id in
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

<where> 的妙处:如果两个 if 都为空,它会自动去掉 where,不会生成 select * from user where 这种残缺 SQL;如果第一个 if 不成立、第二个成立,它会自动把开头的 and 去掉。这些边界处理,自己手写拼接时最容易出错。

五、缓存

MyBatis 有两级缓存,理解它们的作用域就不会混:

级别作用域默认说明
一级缓存SqlSession(一次会话)开启同一次会话查相同 SQL 不重复查库
二级缓存namespace(全局,跨会话)关闭需手动开启,多个 SqlSession 共享

一级缓存:同一个 SqlSession 里,第一次 selectById(1) 查库,第二次再查直接拿缓存,不发 SQL。因为作用域是"一次会话",会话关了缓存就没了,所以天然安全,不会有跨请求脏数据问题。

二级缓存:默认关闭,要开启得在 mapper.xml 加 <cache/>,且实体要实现 Serializable。它跨会话共享,所以更新操作必须清缓存flushCache),否则读到旧数据——这也是为什么很多人干脆用 Redis 替代二级缓存(见《Redis 快速入门》)。

六、Spring Boot 集成

实际项目里几乎没人手写 SqlSessionFactoryBuilder 了,都是 Spring Boot 一键接入:

1. 加依赖mybatis-spring-boot-starter,内含自动配置):

xml
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.3</version>
</dependency>

2. 配置(application.yml):

yaml
mybatis:
  mapper-locations: classpath:mapper/*.xml   # XML 位置
  type-aliases-package: com.demo.entity       # 实体别名包
  configuration:
    map-underscore-to-camel-case: true        # 下划线转驼峰

3. 启动类加 @MapperScan(自动扫描 Mapper 接口):

java
@MapperScan("com.demo.mapper")
@SpringBootApplication
public class Application { ... }

这三步做完,就能直接 @Autowired 注入 UserMapper 用了。背后是 Spring Boot 的自动配置(MybatisAutoConfiguration)在帮你建 SqlSessionFactory——和《Spring Boot 自动配置原理》里讲的机制一脉相承。

小结

  • MyBatis 是半自动 ORM:SQL 自己写,参数绑定/结果映射/连接管理它包了
  • 核心链:SqlSessionFactory(单例)→ SqlSession(方法级)→ Mapper(动态代理)
  • 映射:XML(复杂/动态)vs 注解(简单);#{} 预编译防注入,${} 拼接有风险
  • 动态 SQL 的 <where> <if> <foreach> 解决条件拼接的边界坑
  • 缓存:一级缓存会话级默认开,二级缓存跨会话要手动开且注意脏数据
  • Spring Boot 集成三步:依赖 + @MapperScan + yml 配置

下一步可以看 MyBatis-Plus——它在 MyBatis 之上封装了 BaseMapper(免写 SQL 的 CRUD)、条件构造器、分页插件,还有我 EAM 项目里实际用到的多租户拦截器,那是生产级的高级用法(文章整理中)。