MyBatis Mapper 主链:接口、SQL 映射与事务连接
OrderMapper.find(1) 对应哪条 SQL,由接口名、方法名和映射配置共同确定:
Java 接口:example.OrderMapper
└─ 方法:find
Mapper XML
└─ namespace="example.OrderMapper"
└─ select id="find"
完整语句标识:example.OrderMapper.findMyBatis 负责把方法参数交给 SQL,把查询结果映射回 Java 对象。SQL 的选择、连接的事务归属和对象的生命周期各有具体承担者;顺着这几个对象理解,既能配置第一条查询,也能判断错误发生在映射查找、参数绑定还是数据库执行阶段。
Mapper 接口怎样获得可执行的 SQL
MyBatis 在数据访问中承担什么
直接使用 JDBC 时,应用创建语句、绑定参数、执行并逐列读取结果。MyBatis 保留显式 SQL,用映射配置组织这些重复操作;数据库事务仍由底层连接执行。它适合需要直接控制 SQL、连接查询和方言能力的场景。
JPA/Hibernate 更关注受管实体、持久化上下文和对象变化如何转为 SQL;两种方式的编程模型不同。选型时应看主要查询形态、领域对象关系以及团队是否需要显式控制每条 SQL,而非把 JDBC、MyBatis、JPA 排成固定的学习等级。MyBatis 入门与对象作用域
常见映射入口有三种:
| 入口 | 编写位置 | 主要适用场景 |
|---|---|---|
| Mapper XML | namespace 下的 select/update 等元素 | 复杂 SQL、结果映射和动态结构 |
| SQL 注解 | 接口方法上的 @Select、@Update 等 | 简单语句,配置靠近方法 |
| Provider | 注解指定的 SQL 提供方法 | 在 Java 中组织 SQL 生成逻辑 |
接口注册与 SQL 定义是两件配套工作。MyBatis 注册接口时可以查找同包同名 XML,也可以解析注解;Spring 扫描则创建 Mapper 代理 bean。只有一个空接口被扫描到,不能保证每个方法都已经存在对应语句。
启动期建立的注册表
构建 SqlSessionFactory 时,配置解析器读入数据源、事务工厂、类型处理器和 Mapper 资源。SQL 语句登记为 MappedStatement,以完整的 namespace.id 放进 Configuration;接口则进入 MapperRegistry。MyBatis 配置说明
Configuration
├─ environment 数据源 + 事务工厂
├─ mappedStatements
│ └─ example.OrderMapper.find MappedStatement
├─ resultMaps 列与 Java 属性的对应关系
├─ typeHandlerRegistry Java / JDBC 类型处理器
└─ mapperRegistry
└─ OrderMapper.class MapperProxyFactoryMapperRegistry 保存接口对应的代理工厂。调用 session.getMapper(OrderMapper.class) 时,代理工厂创建一个关联当前 SqlSession 的代理;它不会生成一份包含所有 SQL 业务逻辑的手写实现类。MapperRegistry 实现
同一个完整语句标识必须有唯一含义。将同一方法同时定义在 XML 和注解中,可能造成重复映射;依赖方法重载区分同一个 namespace.id 也不合适。需要不同 SQL 时,使用能表达意图的不同方法名。
从 PostgreSQL 表到第一条 Mapper 查询
环境和工程文件
下载完整实验 ZIP。工程包括纯 MyBatis 和 MyBatis-Spring 两条真实路径:
mybatis-mapper-chain/
├─ pom.xml / compose.yaml / init.sh / run.sh / README.md
└─ src/main/
├─ resources/example/OrderMapper.xml
└─ java/example/
├─ Order.java
├─ OrderMapper.java
├─ Factories.java
├─ TracePlugin.java
├─ OrderService.java
├─ SpringConfig.java
└─ MapperLab.java核心依赖为 MyBatis 3.5.19、MyBatis-Spring 4.0.0、pgJDBC 42.7.13;Spring 依赖由 Boot 4.1.1 BOM 管理。使用 Maven 3.9.12、Java 25,编译目标为 17。MyBatis Spring Boot Starter 4.0 系列面向 Java 17 及以上和 Boot 4.0 及以上;旧 Starter 不能只改一个 Spring 版本就视为兼容。MyBatis Starter 兼容表
运行需要 Linux Bash、Docker Engine、Compose、unzip、openssl。使用有 Docker 权限的普通用户:
test "$(id -u)" -ne 0 || exit 1
docker version
docker compose version
unzip mybatis-mapper-chain-lab.zip
cd mybatis-mapper-chain
export LAB_DIR="$PWD"
export LAB_CACHE="$LAB_DIR/.m2-cache"
mkdir -p "$LAB_CACHE"
export LAB_DB_PASSWORD="$(openssl rand -hex 24)"
export LAB_APP_PASSWORD="$(openssl rand -hex 24)"
export LAB_DB_URL='jdbc:postgresql://db:5432/mapper_lab'
docker compose -p da10-mapper up -d --waitPostgreSQL 18.6 使用容器内 999:999,数据在 tmpfs 中,不发布宿主端口。初始化账户为 lab_owner;应用角色 lab_app 取得实验表的增删改查权限。Java 容器使用当前宿主 UID/GID。数据是可丢弃的,不能将 URL 改成真实业务库运行负例。
init.sh 创建表并准备两行数据:
CREATE TABLE purchase_order (
id bigint PRIMARY KEY,
customer_name text NOT NULL,
amount_cents bigint NOT NULL
);
INSERT INTO purchase_order VALUES
(1, 'Alice', 500),
(2, 'Bob', 800);确认表可读:
docker compose -p da10-mapper exec -T db \
psql -X -U lab_owner -d mapper_lab -v ON_ERROR_STOP=1 \
-c "SELECT id, customer_name, amount_cents FROM purchase_order ORDER BY id"应得到 Alice/500 和 Bob/800。失败时先检查 docker compose -p da10-mapper logs --tail=80 db,确认初始化成功,再处理 Java 配置。
把接口、结果对象和 XML 配起来
Order.java 是普通 Java 对象,拥有 id、customerName、amountCents 字段及对应 getter/setter。查询入口定义在 OrderMapper.java:
package example;
import org.apache.ibatis.annotations.Param;
public interface OrderMapper {
Order find(@Param("id") long id);
}上面只展示首次查询方法;下载工程中的同一接口还包含各负例和事务方法。@Param("id") 明确了 XML 查找的参数名,不依赖编译器是否保留 Java 形参名称。
OrderMapper.xml 的核心映射如下:
<mapper namespace="example.OrderMapper">
<resultMap id="order" type="example.Order">
<id column="id" property="id"/>
<result column="customer_name" property="customerName"/>
<result column="amount_cents" property="amountCents"/>
</resultMap>
<select id="find" resultMap="order">
SELECT id, customer_name, amount_cents
FROM purchase_order
WHERE id = #{id}
</select>
</mapper>column 是结果集列名,property 是 Java 属性名。这里使用显式 ResultMap,避免把数据库下划线命名是否自动转驼峰当成隐含前提。#{id} 最终形成 JDBC 问号占位符,值由参数处理器绑定。Mapper XML 与 ResultMap
建立工厂并在会话中调用
Factories.java 创建 pgJDBC DataSource,然后把它与 JDBC 事务工厂放进 Environment:
Configuration configuration = new Configuration(
new Environment("lab", new JdbcTransactionFactory(), dataSource()));
configuration.addInterceptor(trace);
try (InputStream xml = Factories.class.getResourceAsStream("/example/OrderMapper.xml")) {
if (xml == null) throw new IOException("OrderMapper.xml missing from classpath");
new XMLMapperBuilder(xml, configuration, "example/OrderMapper.xml",
configuration.getSqlFragments()).parse();
}
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(configuration);XML 放在 src/main/resources,Maven 会把它复制到运行类路径;只把文件放进 Java 源目录而未配置资源复制,部署后可能找不到它。数据源读取 LAB_DB_URL、LAB_APP_PASSWORD,用户名固定为 lab_app,并设置连接与 socket 超时。
执行查询的范围是:
try (SqlSession session = factory.openSession()) {
OrderMapper mapper = session.getMapper(OrderMapper.class);
Order order = mapper.find(1);
System.out.println(order.getCustomerName());
}默认 openSession() 关闭自动提交。写操作成功后应调用 session.commit();失败时按事务约定回滚并关闭。默认 DefaultSqlSession 不是线程安全对象,其 Mapper 也依附于这个会话,不能随意保存成单例共享给所有请求。SqlSession API 与事务控制
定义运行函数,再执行首次查询:
lab_java() {
docker run --rm \
--user "$(id -u):$(id -g)" --read-only \
--network da10-mapper_default --tmpfs /tmp:rw,exec,mode=1777 \
--mount "type=bind,src=$LAB_DIR,dst=/src,readonly" \
--mount "type=bind,src=$LAB_CACHE,dst=/cache" \
-e LAB_DB_URL -e LAB_APP_PASSWORD -e MAVEN_CONFIG=/tmp/maven \
maven:3.9.12-eclipse-temurin-25 bash /src/run.sh "$@"
}
lab_java firstrun.sh 编译真实源码,构建输出只放在容器临时目录。运行结果为:
statementId=example.OrderMapper.find sqlSource=RawSqlSource parameterSlots=1 customer=Alice amountCents=500
callbacks=[prepare, parameterize, setParameters, query, handleResultSets]第一行检查实际注册的语句、占位参数与映射结果;第二行来自 MyBatis 插件的真实回调记录。TracePlugin.java 只供单线程实验记录顺序,不作为生产并发日志插件直接使用。
一次方法调用经过哪些对象
MapperProxy 如何分派接口方法
接口代理收到 find(1) 后,从方法缓存中取得或创建对应调用器。普通映射方法使用 MapperMethod,其中 SqlCommand 确定完整语句标识和 SQL 类型,MethodSignature 处理参数与返回值形态。接口 default 方法和 Object 方法走不同分支,不能把代理上的每个方法调用都画成一次 SQL。MapperProxy 实现
方法参数随后被转换成 MyBatis 可以查找的参数对象。单值、多参数、集合、@Param 和 Java 实际形参名存在不同规则;明确命名的参数能减少 XML 与 Java 方法之间的隐含约定。参数对象生成后,MapperMethod 根据语句类型调用 SqlSession 的查询或更新入口。MapperMethod 实现
返回类型也参与分派:集合走多行结果,单对象走单结果,Cursor 走惰性读取;Optional<T> 包装可缺失单值,更新方法则可以接收受影响行数等结果。方法签名必须与 SQL 的真实基数一致。
MappedStatement、SqlSource 与 BoundSql
三个对象的职责可以这样区分:
| 对象 | 保存什么 | 何时使用 |
|---|---|---|
| MappedStatement | 完整 id、语句类型、SQL 来源、结果映射及超时等元数据 | 配置解析后按 id 查找 |
| SqlSource | 根据参数取得 SQL 的规则 | 每次执行需要生成或取得 BoundSql |
| BoundSql | 本次 SQL 文本、参数映射列表及附加参数 | 创建语句、绑定值和生成缓存键 |
首次查询没有动态节点,因此 SqlSource 为 RawSqlSource。BoundSql.getSql() 中看到的是含 ? 的 SQL,不是把所有参数明文拼接进去的“最终 SQL”。参数映射给出每个位置取哪个值、由哪类 TypeHandler 处理。
动态 SQL 会根据本次输入改变语句结构,foreach 等节点还可能生成附加参数;这一部分在 MyBatis 动态 SQL 与 TypeHandler中展开。调试时同时看 SQL 结构和参数映射,避免只打印一个 SQL 字符串后遗漏错误的参数名或 JDBC 类型。
Executor 与 JDBC 执行回调
Executor 负责查询、更新、缓存协调和语句执行策略。在确实需要访问数据库时,执行流程才进入 StatementHandler。一次未命中缓存的普通查询可以概括为:
回调中的 prepare → parameterize → setParameters → query → handleResultSets 对应语句创建、参数化、绑定、查询和结果映射。插件在目标方法进入时记录事件,因此 query 出现在结果处理之前。
缓存命中可以跳过下层 JDBC 回调,不能把缺少 prepare 直接判为插件失效。SIMPLE、REUSE 和 BATCH 也会改变创建语句与发送的时机。二级缓存包装在基础执行器外,缓存检查不能全部放在 JDBC 返回之后;详细规则见 MyBatis Executor、缓存与插件。
接入 Spring 后,会话由谁打开和关闭
SqlSessionTemplate 连接事务与 Mapper
Spring 项目中通常共享 SqlSessionFactory 和 SqlSessionTemplate,再把基于 Template 的 Mapper 注入业务服务。Template 为每次调用寻找当前事务关联的 SqlSession,或在没有事务时管理一次短会话,因此它的可共享性不能外推到手工打开的 DefaultSqlSession。MyBatis-Spring SqlSession
SpringConfig.java 给出了完整可运行配置,其核心关系如下:
@Configuration
@EnableTransactionManagement
public class SpringConfig {
@Bean
public DataSource dataSource() {
return Factories.dataSource();
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource source, TracePlugin trace)
throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(source);
factory.setMapperLocations(new ClassPathResource("example/OrderMapper.xml"));
factory.setPlugins(trace);
return factory.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory factory) {
return new SqlSessionTemplate(factory);
}
@Bean
public OrderMapper orderMapper(SqlSessionTemplate template) {
return template.getMapper(OrderMapper.class);
}
@Bean
public JdbcTransactionManager transactionManager(DataSource source) {
return new JdbcTransactionManager(source);
}
}下载源码还声明 TracePlugin 和 OrderService bean。这里手工取得 Mapper 是为了让对象关系可见;实际应用也可使用 @MapperScan 批量注册。SqlSessionFactoryBean 配置
事务管理器与 SqlSessionFactory 必须使用相匹配的 DataSource。Spring 通过资源绑定把数据库连接与当前事务关联,MyBatis-Spring 使用该事务连接;只让两个数据源写着相同 URL,还没有满足这个条件。MyBatis-Spring 事务要求
验证同一事务确实复用了连接
OrderService.java 的公开方法带 @Transactional,从 Spring 容器取得代理后调用:
@Transactional
public boolean sameSession() {
int first = mapper.backendPid();
int second = mapper.backendPid();
return first == second;
}PID 查询专门关闭二级缓存,并要求清理本地查询缓存:
<select id="backendPid" resultType="int" flushCache="true" useCache="false">
SELECT pg_backend_pid()
</select>否则第二次读取的 PID 可能只是第一次的缓存结果,无法说明第二次数据库调用用了哪条连接。实验同时断言实际发生两次 prepare:
lab_java springspringTransactionSameBackend=true pidQueriesPrepared=2
secondWriteFailed=true firstWriteRolledBack=true
templateManualCommitRejected=true
outsideTransactionCallCommitted=true第二项来自真实事务失败:先将订单金额更新为 1234,再制造主键冲突;独立 JDBC 查询看到金额恢复为 500。第三项确认 Template 拒绝手工 commit()。最后一项在没有 Spring 事务时调用 Mapper 更新为 600,独立连接能看到结果,随后恢复实验数据。
没有事务时,MyBatis-Spring 会完成本次调用的会话管理和提交。把多次 Mapper 调用放进一个普通方法,不会自动为这个方法生成整体事务。事务代理是否生效、异常是否穿过代理,以及同类自调用等问题,应结合 Spring 事务中的规则检查。
相同数据库、不同 DataSource 的反例
运行:
lab_java wrong-datasource实验建立两份指向同一数据库的 DataSource。事务管理器使用第一份,另一份被装进新的 SqlSessionFactory;在第一份的事务中调用第二份的 Mapper,然后主动抛出异常触发回滚。
sameDatabaseDifferentDataSource=true wrongConnectionWriteSurvivedRollback=true第二份 DataSource 新开的连接保持自动提交,订单更新已经提交;第一份连接回滚,无法撤销另一条连接的写入。测试最后恢复金额为 500。
修复时统一相关数据访问对象的 DataSource 注入,或为不同数据源明确配置对应事务管理器与调用边界。不要用“URL 一致”“线程相同”代替这项检查。事务框架里还存在代理数据源、延迟取连接等组合,需按实际包装与资源绑定关系判断,不能仅按类名推测。
Spring Boot 中的常用配置位置
Boot 项目使用官方 Starter,可以省去多数基础 bean 声明:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>4.0.0</version>
</dependency>配套的 Boot 父 POM 或 BOM 为 4.1.1,MyBatis 和 MyBatis-Spring 版本由兼容的 Starter 管理。常用配置包括:
spring:
datasource:
url: ${DB_URL}
username: ${DB_USER}
password: ${DB_PASSWORD}
mybatis:
mapper-locations: classpath*:mapper/**/*.xml
configuration:
map-underscore-to-camel-case: true这个资源路径只是 Boot 项目的目录约定;本实验的资源在 example/OrderMapper.xml,不能不移动文件就照抄成 mapper/**/*.xml。配置了自动转驼峰后,复杂结果仍可使用显式 ResultMap。多数据源项目应核对每个 Mapper 扫描器实际关联的 SqlSessionFactory,而非期待全局配置自动区分。
按失败位置检查映射和资源
接口存在,但没有对应语句
执行:
lab_java missing实验注册一个没有注解、也没有同包同名 XML 的接口,调用时得到 BindingException:
interfaceRegistered=true statementMissingRejected=true检查实际部署产物中是否有 Mapper XML,再核对 namespace、方法名及该 Mapper 使用的 SqlSessionFactory。XML 存在于源码仓库,却没进入 classpath,和 SQL 写错是不同的问题;此时修改数据库表没有帮助。
参数名错误与结果基数错误
参数负例的 Java 方法使用 @Param("key"),XML 却仍引用 #{id}:
lab_java parametersparamNameMismatchRejected=true expectedName=id修复位置是 Java 参数名约定与 XML 引用,不是 JDBC 连接配置。异常可能被上层 PersistenceException 包装,定位时要检查 cause 链中的 BindingException。
如果方法声明返回一个 Order,但 SQL 返回两行:
lab_java cardinalityselectOneRejectedMultipleRows=trueMyBatis 抛出 TooManyResultsException。先判断业务查询是否本应唯一:若应该唯一,检查条件、连接放大和约束;若本来就是多条结果,把返回类型改为集合。简单加 LIMIT 1 会隐藏数据或条件错误,而且没有确定排序时返回哪一行也不稳定。
游标要在会话存续期间消费
Mapper 返回 Cursor<Order> 时,结果是惰性消费的。把 Cursor 从已关闭的 SqlSession 中带出去,读取过程失去资源来源:
lab_java cursorclosedSessionCursorRejected=true
cursorConsumedInSession=true rows=2正确使用范围是:
try (SqlSession session = factory.openSession();
Cursor<Order> rows = session.getMapper(OrderMapper.class).stream()) {
for (Order order : rows) {
process(order);
}
}process 代表在当前范围内处理一行的业务方法。若底层还需要真正分块取数,必须满足驱动的游标条件,详见 JDBC 事务与批处理中的 fetchSize。仅换成 Cursor 返回类型,无法抵消消费端重新积累全部对象或在无事务 Template 调用结束后才开始迭代的问题。
排查时记录完整 statement id、SQL 结构、参数映射和第一条异常;敏感值只记录受控摘要。日志里的“Preparing”只能说明经过了某个驱动准备阶段,真正是否执行、是否返回和是否提交,还要对照后续事件。
结束实验:
docker compose -p da10-mapper ps
docker compose -p da10-mapper down
unset LAB_DB_PASSWORD LAB_APP_PASSWORD LAB_DB_URL
unset -f lab_java这会删除本实验容器和网络,tmpfs 数据不可恢复;源码和依赖缓存保留。
权威资料与规范地址
API、映射规则和 Spring 接入使用 MyBatis 官方文档;实现细节可通过对应源码入口查阅。实验固定 MyBatis 3.5.19、MyBatis-Spring 4.0.0,升级时需复跑回调顺序和事务负例。
| 查阅对象 | 官方地址 |
|---|---|
| MyBatis 入门和对象作用域 | https://mybatis.org/mybatis-3/getting-started.html |
| MyBatis 配置 | https://mybatis.org/mybatis-3/configuration.html |
| Mapper XML 与结果映射 | https://mybatis.org/mybatis-3/sqlmap-xml.html |
| SqlSession Java API | https://mybatis.org/mybatis-3/java-api.html |
| MapperRegistry | https://mybatis.org/mybatis-3/xref/org/apache/ibatis/binding/MapperRegistry.html |
| MapperProxy | https://mybatis.org/mybatis-3/xref/org/apache/ibatis/binding/MapperProxy.html |
| MapperMethod | https://mybatis.org/mybatis-3/xref/org/apache/ibatis/binding/MapperMethod.html |
| MyBatis-Spring SqlSessionTemplate | https://mybatis.org/spring/sqlsession.html |
| SqlSessionFactoryBean | https://mybatis.org/spring/factorybean.html |
| MyBatis-Spring 事务 | https://mybatis.org/spring/transactions.html |
| MyBatis Spring Boot Starter | https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/ |
