MyBatis 基础认知、配置体系与核心映射

本文件覆盖 MyBatis 从入门到高级的基础能力:它解决什么问题、和 JDBC / JPA 的区别、核心运行流程、全局配置、Mapper 注册、参数绑定、基本 CRUD、最小 Demo,以及入门到专家的基础面试题答案。

官方参考:

1. MyBatis 解决什么问题

MyBatis 是一个半自动 ORM / SQL Mapper 框架。它不试图隐藏 SQL,而是让开发者显式编写 SQL,并负责:

  • 创建和管理 JDBC PreparedStatement
  • 把 Java 参数绑定到 SQL。
  • 执行 SQL。
  • ResultSet 映射为 Java 对象。
  • 管理一级缓存、二级缓存、插件拦截、TypeHandler 等机制。

传统 JDBC 代码:

java 复制代码
String sql = "select id, username, email from users where id = ?";
try (Connection conn = dataSource.getConnection();
     PreparedStatement ps = conn.prepareStatement(sql)) {
  ps.setLong(1, id);
  try (ResultSet rs = ps.executeQuery()) {
    if (rs.next()) {
      User user = new User();
      user.setId(rs.getLong("id"));
      user.setUsername(rs.getString("username"));
      user.setEmail(rs.getString("email"));
      return user;
    }
  }
}

MyBatis Mapper:

java 复制代码
public interface UserMapper {
  User selectById(Long id);
}
xml 复制代码
<select id="selectById" parameterType="long" resultType="com.example.User">
  select id, username, email
  from users
  where id = #{id}
</select>

MyBatis 的价值是减少 JDBC 样板代码,同时保留 SQL 控制权。

2. MyBatis、JDBC、JPA 的区别

维度 JDBC MyBatis JPA/Hibernate
SQL 控制 完全手写 手写 SQL 为主 框架生成 SQL 为主
映射能力 手写 ResultSet XML/注解映射 Entity 映射
学习成本 低但样板多 中等
复杂查询 可控但繁琐 强,可直接优化 SQL 复杂查询可能绕
性能可控性 依赖 ORM 策略
适用场景 小工具/底层 业务系统、复杂 SQL 领域模型驱动、CRUD 多

MyBatis 适合:

  • SQL 较复杂。
  • 报表、搜索、条件查询多。
  • 需要精确控制 SQL 性能。
  • 团队熟悉数据库。
  • 不希望 ORM 自动生成不可控 SQL。

不适合:

  • 完全不想写 SQL。
  • 领域模型关系复杂且希望 ORM 自动管理对象图。
  • 业务几乎都是简单 CRUD 且团队更偏 JPA。

3. MyBatis 核心运行流程

典型流程:

text 复制代码
Mapper 方法调用
  -> MapperProxy
  -> MappedStatement
  -> SqlSource 生成 BoundSql
  -> ParameterHandler 绑定参数
  -> StatementHandler 执行 SQL
  -> ResultSetHandler 处理结果集
  -> TypeHandler 做 Java/JDBC 类型转换

关键概念:

  • SqlSessionFactory:创建 SqlSession 的工厂。
  • SqlSession:执行 SQL、获取 Mapper、管理一级缓存的会话对象。
  • Mapper:Java 接口,方法与 SQL statement 对应。
  • MappedStatement:一条 SQL 语句的完整元数据。
  • BoundSql:动态 SQL 解析后的最终 SQL 和参数映射。
  • Executor:执行器,负责查询、更新、缓存等。

4. 最小配置 Demo:纯 MyBatis

目录:

text 复制代码
mybatis-demo/
├── pom.xml
├── src/main/java/com/example/User.java
├── src/main/java/com/example/UserMapper.java
├── src/main/java/com/example/Main.java
└── src/main/resources/
    ├── mybatis-config.xml
    └── mapper/UserMapper.xml

Maven 依赖:

xml 复制代码
<dependencies>
  <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.16</version>
  </dependency>
  <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.2.224</version>
  </dependency>
</dependencies>

实体:

java 复制代码
package com.example;

public class User {
  private Long id;
  private String username;
  private String email;

  public Long getId() { return id; }
  public void setId(Long id) { this.id = id; }

  public String getUsername() { return username; }
  public void setUsername(String username) { this.username = username; }

  public String getEmail() { return email; }
  public void setEmail(String email) { this.email = email; }
}

Mapper 接口:

java 复制代码
package com.example;

public interface UserMapper {
  User selectById(Long id);
  int insert(User user);
}

MyBatis 配置:

xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <settings>
    <setting name="mapUnderscoreToCamelCase" value="true"/>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
  </settings>

  <typeAliases>
    <typeAlias alias="User" type="com.example.User"/>
  </typeAliases>

  <environments default="dev">
    <environment id="dev">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="org.h2.Driver"/>
        <property name="url" value="jdbc:h2:mem:test;MODE=MySQL;DB_CLOSE_DELAY=-1"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
      </dataSource>
    </environment>
  </environments>

  <mappers>
    <mapper resource="mapper/UserMapper.xml"/>
  </mappers>
</configuration>

Mapper XML:

xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.UserMapper">
  <select id="selectById" parameterType="long" resultType="User">
    select id, username, email
    from users
    where id = #{id}
  </select>

  <insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id">
    insert into users(username, email)
    values(#{username}, #{email})
  </insert>
</mapper>

启动代码:

java 复制代码
package com.example;

import java.io.Reader;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

public class Main {
  public static void main(String[] args) throws Exception {
    try (Reader reader = Resources.getResourceAsReader("mybatis-config.xml")) {
      SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);

      try (SqlSession session = factory.openSession()) {
        session.update("org.apache.ibatis.jdbc.ScriptRunner"); // 示例中实际应先执行 schema.sql
      }

      try (SqlSession session = factory.openSession(true)) {
        UserMapper mapper = session.getMapper(UserMapper.class);
        User user = new User();
        user.setUsername("ada");
        user.setEmail("ada@example.com");
        mapper.insert(user);

        User loaded = mapper.selectById(user.getId());
        System.out.println(loaded.getUsername());
      }
    }
  }
}

说明:真实 Demo 中建议用 H2 初始化脚本或 Spring Boot 自动执行 schema.sql,这里重点展示 MyBatis 最小运行链路。

5. 全局配置结构

官方配置顶层结构包括:

xml 复制代码
<configuration>
  <properties/>
  <settings/>
  <typeAliases/>
  <typeHandlers/>
  <objectFactory/>
  <plugins/>
  <environments/>
  <databaseIdProvider/>
  <mappers/>
</configuration>

常用配置:

xml 复制代码
<settings>
  <setting name="mapUnderscoreToCamelCase" value="true"/>
  <setting name="cacheEnabled" value="true"/>
  <setting name="lazyLoadingEnabled" value="false"/>
  <setting name="defaultExecutorType" value="SIMPLE"/>
  <setting name="logImpl" value="SLF4J"/>
</settings>

关键含义:

  • mapUnderscoreToCamelCaseuser_name 自动映射到 userName
  • cacheEnabled:全局二级缓存开关。
  • lazyLoadingEnabled:延迟加载开关。
  • defaultExecutorType:默认执行器,SIMPLEREUSEBATCH
  • logImpl:日志实现。

6. Mapper XML 的核心元素

Mapper XML 常用元素:

  • cache:命名空间二级缓存。
  • cache-ref:引用其他 namespace 缓存。
  • resultMap:复杂结果映射。
  • sql:可复用 SQL 片段。
  • select:查询。
  • insert:插入。
  • update:更新。
  • delete:删除。

示例:

xml 复制代码
<mapper namespace="com.example.UserMapper">
  <sql id="BaseColumns">
    id, username, email, created_at
  </sql>

  <select id="selectAll" resultType="User">
    select <include refid="BaseColumns"/>
    from users
  </select>
</mapper>

7. #{}${} 的区别

#{} 使用 PreparedStatement 参数绑定,安全,防 SQL 注入:

xml 复制代码
where username = #{username}

${} 是字符串拼接,危险:

xml 复制代码
order by ${sort}

#{} 会生成:

sql 复制代码
where username = ?

${} 会直接拼接:

sql 复制代码
order by created_at desc

专家实践:业务值必须使用 #{}。只有列名、排序方向、表名等无法参数化的位置才考虑 ${},并必须做白名单校验。

白名单示例:

java 复制代码
public enum UserSortField {
  ID("id"),
  CREATED_AT("created_at");

  private final String column;

  UserSortField(String column) {
    this.column = column;
  }

  public String column() {
    return column;
  }
}

XML:

xml 复制代码
order by ${sortField.column} ${sortDirection}

其中 sortDirection 也必须由枚举或白名单生成。

8. 参数绑定

单参数:

java 复制代码
User selectById(Long id);
xml 复制代码
where id = #{id}

多参数建议使用 @Param

java 复制代码
List<User> selectByStatusAndKeyword(
  @Param("status") String status,
  @Param("keyword") String keyword
);
xml 复制代码
where status = #{status}
and username like concat('%', #{keyword}, '%')

对象参数:

java 复制代码
int insert(User user);
xml 复制代码
values(#{username}, #{email})

Map 参数可用但不推荐在复杂业务中过度使用,因为类型不安全。

9. CRUD 最小 Demo

Mapper:

java 复制代码
public interface ProductMapper {
  Product selectById(Long id);
  List<Product> selectAll();
  int insert(Product product);
  int update(Product product);
  int deleteById(Long id);
}

XML:

xml 复制代码
<mapper namespace="com.example.ProductMapper">
  <select id="selectById" resultType="Product">
    select id, name, price, stock
    from products
    where id = #{id}
  </select>

  <select id="selectAll" resultType="Product">
    select id, name, price, stock
    from products
    order by id desc
  </select>

  <insert id="insert" parameterType="Product" useGeneratedKeys="true" keyProperty="id">
    insert into products(name, price, stock)
    values(#{name}, #{price}, #{stock})
  </insert>

  <update id="update" parameterType="Product">
    update products
    set name = #{name},
        price = #{price},
        stock = #{stock}
    where id = #{id}
  </update>

  <delete id="deleteById">
    delete from products
    where id = #{id}
  </delete>
</mapper>

10. 注解 Mapper

简单 SQL 可用注解:

java 复制代码
public interface UserMapper {
  @Select("select id, username, email from users where id = #{id}")
  User selectById(Long id);

  @Insert("""
    insert into users(username, email)
    values(#{username}, #{email})
    """)
  @Options(useGeneratedKeys = true, keyProperty = "id")
  int insert(User user);
}

注解适合:

  • SQL 很短。
  • 查询不复杂。
  • Demo 或简单 CRUD。

XML 适合:

  • 动态 SQL 多。
  • ResultMap 复杂。
  • SQL 需要审查和调优。
  • 团队希望 SQL 与 Java 分离。

11. 一级缓存

一级缓存是 SqlSession 级别缓存,默认开启。

java 复制代码
try (SqlSession session = factory.openSession()) {
  UserMapper mapper = session.getMapper(UserMapper.class);
  User a = mapper.selectById(1L);
  User b = mapper.selectById(1L);
}

同一个 SqlSession 中,两次相同查询可能命中一级缓存。

注意:

  • insert/update/delete 会清理缓存。
  • 不同 SqlSession 不共享一级缓存。
  • Spring 集成中 SqlSession 通常由事务边界管理。

12. 入门到专家知识点清单

入门:

  • MyBatis 定位。
  • Mapper 接口。
  • Mapper XML。
  • #{} 参数绑定。
  • 基础 CRUD。
  • SqlSessionFactory / SqlSession

进阶:

  • 全局配置。
  • @Param
  • 注解 Mapper。
  • SQL 片段。
  • 一级缓存。
  • XML 与注解取舍。

高级:

  • MappedStatement
  • BoundSql
  • Executor
  • ParameterHandler
  • ResultSetHandler
  • TypeHandler。

精通:

  • Spring Boot 集成。
  • 事务边界。
  • 批量执行。
  • 插件机制。
  • 二级缓存。
  • SQL 性能调优。

专家:

  • Mapper 分层治理。
  • SQL 安全策略。
  • 多数据源。
  • 读写分离。
  • 分库分表边界。
  • 数据访问层架构设计。

13. 面试题与完整答案

13.1 MyBatis 是 ORM 吗?

MyBatis 通常被称为半自动 ORM 或 SQL Mapper。它不像 Hibernate 那样自动生成大部分 SQL,而是由开发者显式编写 SQL,MyBatis 负责参数绑定、执行、结果映射、缓存和扩展。它更强调 SQL 可控性。

13.2 MyBatis 和 JDBC 的区别是什么?

JDBC 需要手写连接、Statement、参数绑定、ResultSet 映射、异常处理和资源释放。MyBatis 封装了这些样板代码,让开发者专注 SQL 和映射关系。同时 MyBatis 提供 Mapper 接口、动态 SQL、ResultMap、缓存和插件机制。

13.3 #{}${} 的区别是什么?

#{} 会使用 PreparedStatement 参数占位符,安全防注入;${} 是字符串拼接,会直接进入 SQL。业务值必须用 #{},只有列名、排序方向等无法参数化的位置才可能使用 ${},并且必须做白名单校验。

13.4 为什么建议多参数使用 @Param

@Param 能给参数明确命名,使 XML 中可以用 #{status}#{keyword} 访问,避免依赖 MyBatis 默认参数名如 param1arg0,提高可读性和可维护性。

13.5 XML 和注解 Mapper 如何选择?

简单 SQL 可用注解,代码集中、阅读方便。复杂动态 SQL、复杂 ResultMap、SQL 需要审查或调优时优先 XML。大型项目通常 XML 更可维护,因为 SQL 可以独立组织和优化。

13.6 一级缓存是什么?

一级缓存是 SqlSession 级缓存,同一个 SqlSession 中相同查询可复用结果。它默认开启,insert/update/delete 会清理缓存。Spring 场景下一级缓存通常受事务和 SqlSession 生命周期影响。

13.7 MyBatis 的核心执行链路是什么?

Mapper 方法通过动态代理进入 MyBatis,找到对应 MappedStatement,由 SqlSource 生成 BoundSqlParameterHandler 绑定参数,StatementHandler 执行 SQL,ResultSetHandler 映射结果,TypeHandler 负责 Java/JDBC 类型转换。

13.8 MyBatis 最大的优势和风险是什么?

优势是 SQL 可控、性能可调、复杂查询表达能力强。风险是 SQL 分散、动态 SQL 复杂后难维护、${} 容易注入、N+1 查询、Mapper 边界混乱。专家级使用 MyBatis 的关键是建立数据访问层规范。

14. 配置项扩展矩阵

MyBatis 的全局配置对运行行为影响很大。以下配置在工程中最常见。

配置 作用 默认倾向 专家建议
mapUnderscoreToCamelCase 下划线转驼峰 false Java 项目常开启
cacheEnabled 二级缓存总开关 true 开启不代表所有 Mapper 都缓存
lazyLoadingEnabled 延迟加载 false 谨慎开启,避免隐藏 N+1
aggressiveLazyLoading 激进延迟加载 false 通常保持 false
defaultExecutorType 执行器类型 SIMPLE 批处理单独设计
defaultStatementTimeout SQL 超时时间 unset 生产建议设置
defaultFetchSize JDBC fetch size unset 大结果集可设置
safeRowBoundsEnabled 嵌套语句 RowBounds 安全 false 不推荐依赖 RowBounds 分页
localCacheScope 一级缓存范围 SESSION 高一致性场景可改 STATEMENT
jdbcTypeForNull null 参数 JDBC 类型 OTHER Oracle 等场景常需配置
callSettersOnNulls null 也调用 setter false Map 或特殊对象需要时开启
returnInstanceForEmptyRow 空行返回空对象 false 嵌套映射场景谨慎开启
logImpl 日志实现 自动发现 生产用 SLF4J

示例:

xml 复制代码
<settings>
  <setting name="mapUnderscoreToCamelCase" value="true"/>
  <setting name="defaultStatementTimeout" value="5"/>
  <setting name="defaultFetchSize" value="200"/>
  <setting name="localCacheScope" value="SESSION"/>
  <setting name="logImpl" value="SLF4J"/>
</settings>

15. Mapper 方法与 Statement 匹配规则

Mapper 接口:

java 复制代码
package com.example.mapper;

public interface UserMapper {
  User selectById(Long id);
}

XML:

xml 复制代码
<mapper namespace="com.example.mapper.UserMapper">
  <select id="selectById" resultType="User">
    select id, username from users where id = #{id}
  </select>
</mapper>

匹配关系:

text 复制代码
namespace + "." + statement id
= com.example.mapper.UserMapper.selectById

常见错误:

  • namespace 写错。
  • XML 文件未被扫描。
  • 方法名和 statement id 不一致。
  • Mapper 接口包名变了但 XML 没改。
  • 重载方法导致 statement id 冲突。

专家建议:Mapper 方法不要重载。MyBatis statement id 只按方法名匹配,重载会造成语义混乱。

16. 参数命名深入

@Param 的多参数方法:

java 复制代码
User select(String username, String email);

MyBatis 可能提供:

text 复制代码
arg0, arg1, param1, param2

不推荐在 XML 中依赖这些名字。

推荐:

java 复制代码
User select(@Param("username") String username, @Param("email") String email);

XML:

xml 复制代码
where username = #{username}
and email = #{email}

集合参数:

java 复制代码
List<User> selectByIds(@Param("ids") List<Long> ids);

XML:

xml 复制代码
<foreach collection="ids" item="id" open="(" separator="," close=")">
  #{id}
</foreach>

17. 主键回填详解

自增主键:

xml 复制代码
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
  insert into users(username, email)
  values(#{username}, #{email})
</insert>

插入后:

java 复制代码
User user = new User();
user.setUsername("ada");
mapper.insert(user);
Long id = user.getId();

非自增主键可在业务层生成:

java 复制代码
user.setId(idGenerator.nextId());
mapper.insert(user);

专家建议:

  • 分布式系统优先使用雪花 ID、号段模式或数据库序列。
  • 自增 ID 简单,但分库分表和数据迁移时会受限。
  • 主键生成策略属于架构决策,不只是 Mapper 配置。

18. Java 类型与 JDBC 类型

参数可指定 JDBC 类型:

xml 复制代码
where deleted_at is #{deletedAt,jdbcType=TIMESTAMP}

null 值场景有些数据库需要明确 JDBC 类型:

xml 复制代码
#{remark,jdbcType=VARCHAR}

常见映射:

Java JDBC
String VARCHAR
Long BIGINT
Integer INTEGER
BigDecimal DECIMAL
LocalDateTime TIMESTAMP
Boolean BOOLEAN / BIT
Enum VARCHAR / INTEGER

19. 最小 Debug Demo:查看最终 SQL

拦截器或日志可查看 BoundSql。

java 复制代码
MappedStatement ms = configuration.getMappedStatement(
  "com.example.UserMapper.selectById"
);
BoundSql boundSql = ms.getBoundSql(1L);
System.out.println(boundSql.getSql());
System.out.println(boundSql.getParameterMappings());

理解 BoundSql 有助于排查:

  • 动态 SQL 最终生成什么。
  • 参数名是否正确。
  • foreach 是否展开。
  • include 是否生效。

20. 基础反模式扩展

20.1 Mapper 返回 Map

java 复制代码
Map<String, Object> selectUser(Long id);

问题:

  • 类型不安全。
  • 字段名散落业务层。
  • 重构困难。
  • IDE 无法帮助检查。

只在通用报表、临时工具或字段不固定场景使用。

20.2 select *

风险:

  • 表结构变化影响对象映射。
  • 多取无用列。
  • 覆盖索引失效。
  • join 时列名冲突。

推荐明确列名。

20.3 Mapper 方法语义不清

不推荐:

java 复制代码
List<User> query(Map<String, Object> params);

推荐:

java 复制代码
List<User> searchActiveUsers(UserSearchQuery query);

21. 基础阶段专家题补充

21.1 为什么 MyBatis 更适合 SQL 能力强的团队?

MyBatis 把 SQL 控制权交给开发者,复杂查询、索引、分页、锁和执行计划都需要团队理解数据库。如果团队 SQL 能力弱,可能写出大量慢 SQL、N+1 查询和注入风险。MyBatis 的自由度越高,对规范和审查要求越高。

21.2 为什么不要让数据库表结构直接决定领域模型?

数据库表是持久化结构,领域模型表达业务规则。两者生命周期不同。表结构可能为了性能、历史兼容或报表做妥协,领域模型应表达业务概念。复杂系统中应通过 Repository 或 Assembler 做转换。

21.3 MyBatis 是否会自动管理对象关系?

不会像 Hibernate 那样自动管理持久化上下文和对象关系。MyBatis 可以通过 ResultMap 映射 association/collection,但查询时机和 SQL 仍由开发者设计。这让性能更可控,也要求开发者主动处理对象图加载策略。

相关推荐
空中海2 小时前
05 MyBatis 架构设计、渐进式综合项目与专家题库
mybatis
空中海4 小时前
03 MyBatis Spring Boot 集成、事务、测试与工程化体系
spring boot·后端·mybatis
Nicander2 天前
理解 mybatis 源码:vibe-coding一个mini-mybatis
后端·mybatis
庞轩px2 天前
致远互联实习复盘:一条SQL替代300次循环查询,组织架构选择器从5秒降到300毫秒
java·sql·mysql·mybatis·实习经历·n+1问题·join联表查询
952363 天前
MyBatis
后端·spring·mybatis
misL NITL4 天前
idea、mybatis报错Property ‘sqlSessionFactory‘ or ‘sqlSessionTemplate‘ are required
tomcat·intellij-idea·mybatis
是宇写的啊4 天前
MyBatis-Plus
java·开发语言·mybatis
工作log5 天前
Spring Boot 3.5 + MyBatis Plus + RabbitMQ:打造 AI 驱动的慢 SQL 监控与优化系统
spring boot·mybatis·java-rabbitmq
河阿里5 天前
MyBatis-Plus:MyBatis的进阶开发
数据库·mybatis