0%

mybatis之MappedStatement

MyBatis MappedStatement 深度解析:Mapper 节点的 “配置容器” 与 SQL 执行的 “元数据核心”

在 MyBatis 中,MappedStatementMapper 配置(XML / 注解)与 SQL 执行之间的关键桥梁—— 它将 Mapper 中单个 SQL 节点(如 <select><insert>)的所有配置(SQL 语句、参数映射、结果映射、缓存策略等)封装为一个可执行的 “元数据对象”,是 MyBatis 解析配置、生成 SQL、执行查询的核心依据。从 “核心定位→属性解析→生命周期→实战关联” 四个维度,彻底拆解 MappedStatement 的作用与价值。

MappedStatement 核心定位:SQL 节点的 “配置容器”

MyBatis 启动时,会扫描所有 Mapper 接口和 XML 文件,将每个 SQL 节点(如 <select id="selectUserById"> 解析为一个 MappedStatement 对象,存储在 ConfigurationmappedStatements 集合中(keynamespace + id,如 com.example.UserMapper.selectUserById)。

MappedStatement 的核心作用是:

  1. 配置聚合:集中管理单个 SQL 节点的所有配置(SQL 语句、参数、结果、缓存等),避免配置分散;
  2. 元数据提供:为 Executor(执行器)、StatementHandler(SQL 处理器)等组件提供执行所需的全部元数据;
  3. 一致性保障:确保 SQL 执行过程中,参数绑定、结果映射、缓存策略等配置的一致性。

MappedStatement 核心属性解析

MappedStatement 的属性与 Mapper 节点的配置一一对应,每个属性都直接影响 SQL 的执行逻辑。按 “核心功能分组” 解析关键属性:

1. 基础标识与配置

属性名 类型 对应 Mapper 配置 核心作用
id String SQL 节点的 id(如 selectUserById 唯一标识当前 SQL 节点,与 namespace 组合为全局唯一键(namespace.id
resource String Mapper XML 文件路径(如 com/example/mapper/UserMapper.xml 记录当前 SQL 节点所在的资源文件,用于错误定位(如 “SQL 执行错误:resource=xxx”)
configuration Configuration - 关联 MyBatis 全局配置(如 TypeHandlerRegistryCache 等),提供全局上下文
databaseId String <databaseIdProvider> 配置 多数据库适配标识(如 mysql/oracle),用于选择对应数据库的 SQL 语句

2. SQL 执行相关配置

属性名 类型 对应 Mapper 配置 核心作用
sqlSource SqlSource SQL 节点内的 SQL 语句(如 SELECT * FROM user WHERE id=? 封装解析后的 SQL 语句,支持动态 SQL(如 <if>/<foreach>),可生成 BoundSql 对象
statementType StatementType SQL 节点的 statementType(默认 PREPARED 指定 SQL 执行方式: - STATEMENT:普通 Statement(无预编译) - PREPARED:PreparedStatement(预编译,推荐) - CALLABLE:CallableStatement(存储过程)
fetchSize Integer SQL 节点的 fetchSize 每次从数据库获取的结果行数(优化大数据量查询的网络传输)
timeout Integer SQL 节点的 timeout SQL 执行超时时间(毫秒),超时则抛出 SQLException
resultSetType ResultSetType SQL 节点的 resultSetType(默认 FORWARD_ONLY 指定 ResultSet 的滚动方式: - FORWARD_ONLY:仅向前滚动(默认,性能最优) - SCROLL_INSENSITIVE:可滚动,不敏感于数据变化 - SCROLL_SENSITIVE:可滚动,敏感于数据变化

3. 参数与结果映射配置

属性名 类型 对应 Mapper 配置 核心作用
parameterMap ParameterMap <parameterMap> 节点(已过时,推荐直接在 SQL 中用 #{} 定义参数的映射规则(如参数类型、TypeHandler),现已被 ParameterMapping 替代
resultMaps List<ResultMap> <resultMap> 节点或 resultType 定义结果集到 Java 对象的映射规则(如 column="user_id" property="userId"),支持嵌套映射(association/collection
hasNestedResultMaps boolean 是否包含嵌套 ResultMap(如 association/collection 标记当前 SQL 结果是否有嵌套映射,用于 ResultSetHandler 优化结果解析逻辑
keyGenerator KeyGenerator SQL 节点的 useGeneratedKeys<selectKey> 主键生成器(如 Jdbc3KeyGenerator/SelectKeyGenerator),负责主键回填
keyProperties String[] SQL 节点的 keyProperty 主键对应的 Java 对象属性(如 id),用于主键回填时设置属性值
keyColumns String[] SQL 节点的 keyColumn 主键对应的数据库列名(如 user_id),当数据库列名与属性名不一致时需指定

4. 缓存策略配置

属性名 类型 对应 Mapper 配置 核心作用
cache Cache <cache><cache-ref> 节点 关联当前 Mapper 的二级缓存对象(如 PerpetualCache+LruCache 组合)
useCache boolean SQL 节点的 useCache(默认 true for SELECT) 标记当前 SQL 是否使用二级缓存(SELECT 推荐 true,INSERT/UPDATE/DELETE 默认为 false
flushCacheRequired boolean SQL 节点的 flushCache(默认 false for SELECT,true for 增删改) 标记执行当前 SQL 后是否清空二级缓存(增删改需清空,避免脏数据)

5. 其他辅助配置

属性名 类型 对应 Mapper 配置 核心作用
sqlCommandType SqlCommandType SQL 节点类型(SELECT/INSERT/UPDATE/DELETE/FLUSH 标识 SQL 的操作类型,用于 Executor 选择对应执行逻辑(如 query()/update()
lang LanguageDriver SQL 节点的 lang(默认 XMLLanguageDriver 语言驱动,负责解析 SQL 节点(如 XML 语法或注解语法),支持自定义 SQL 解析规则
resultSets String[] SQL 节点的 resultSets 多结果集命名(仅用于存储过程,为每个结果集指定名称,用逗号分隔)
statementLog Log - 日志对象,用于记录当前 SQL 的执行日志(如执行时间、参数值)

MappedStatement 的生命周期与创建流程

MappedStatement 的生命周期与 MyBatis 应用一致(从应用启动到关闭),其创建流程分为 “解析→验证→注册” 三步:

1. 解析阶段:从 Mapper 配置到对象

MyBatis 启动时,通过 XMLMapperBuilder(解析 XML)或 MapperAnnotationBuilder(解析注解)解析 Mapper 配置,生成 MappedStatement

  • XML 解析:
    1. 读取 <mapper> 节点的 namespace
    2. 遍历 <select>/<insert> 等子节点,解析每个节点的 idsqlresultMapuseCache 等属性;
    3. 将解析后的属性封装为 MappedStatement 对象(如 sqlSourceSqlSourceBuilder 生成,resultMapsResultMapResolver 解析)。
  • 注解解析:
    1. 扫描 Mapper 接口的注解(如 @Select/@Insert);
    2. 将注解中的 SQL 语句和属性(如 @Options(useGeneratedKeys = true))解析为 MappedStatement 属性。

2. 验证阶段:确保配置合法

解析完成后,MyBatis 会验证 MappedStatement 的关键配置:

  • id 必须唯一(同一 namespace 下无重复 id);
  • sqlSource 不能为空(SQL 语句必须有效);
  • resultMapsresultType 必须配置(SELECT 语句需指定结果类型);
  • keyPropertieskeyGenerator 需匹配(如 useGeneratedKeys=true 时必须指定 keyProperty)。

3. 注册阶段:存入 Configuration

验证通过后,MappedStatement 会被注册到 ConfigurationmappedStatements 集合中(ConcurrentHashMap),key 为 namespace + id(如 com.example.UserMapper.selectUserById),供后续执行时获取。

注册源码片段(Configuration 类)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Configuration {
// 存储所有 MappedStatement,key 为 namespace.id
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
.conflictMessageProducer((saved, target) ->
". please check " + saved.getResource() + " and " + target.getResource());

// 注册 MappedStatement
public void addMappedStatement(MappedStatement ms) {
mappedStatements.put(ms.getId(), ms);
}

// 获取 MappedStatement(执行 SQL 时调用)
public MappedStatement getMappedStatement(String id) {
return getMappedStatement(id, true);
}
}

MappedStatement 与核心组件的协作

MappedStatement 是 MyBatis 执行 SQL 的 “元数据中心”,所有核心组件(ExecutorStatementHandlerParameterHandler 等)均依赖它提供的配置:

1. 与 Executor 的协作(执行器)

Executor 是 SQL 执行的调度者,通过 MappedStatement 获取执行所需的关键信息:

  • 获取 SQL 类型:通过 ms.getSqlCommandType() 确定调用 query()(SELECT)或 update()(INSERT/UPDATE/DELETE);
  • 获取缓存配置:通过 ms.isUseCache()ms.getCache() 决定是否使用二级缓存;
  • 获取主键生成器:通过 ms.getKeyGenerator() 调用主键生成逻辑(如 processBefore/processAfter)。
协作源码片段(BaseExecutor 类)
1
2
3
4
5
6
7
8
9
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) throw new ExecutorException("Executor was closed.");
// 1. 根据 ms.isFlushCacheRequired() 决定是否清空缓存
clearLocalCache();
// 2. 调用 StatementHandler 执行更新,传入 MappedStatement
return doUpdate(ms, parameter);
}

2. 与 StatementHandler 的协作(SQL 处理器)

StatementHandler 负责创建 Statement 并执行 SQL,依赖 MappedStatement 配置:

  • 创建 Statement 类型:通过 ms.getStatementType() 选择创建 PreparedStatementCallableStatement
  • 生成 BoundSql:通过 ms.getSqlSource().getBoundSql(parameter) 生成包含实际参数的 SQL 语句;
  • 设置超时与 fetchSize:通过 ms.getTimeout()ms.getFetchSize() 配置 Statement 的执行参数。
协作源码片段(RoutingStatementHandler 类)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 根据 ms.getStatementType() 选择对应的 StatementHandler
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}

3. 与 ResultSetHandler 的协作(结果处理器)

ResultSetHandler 负责结果集映射,依赖 MappedStatementresultMaps 配置:

  • 获取结果映射规则:通过 ms.getResultMaps() 获取 ResultMap,确定 “数据库列→Java 属性” 的映射关系;
  • 处理嵌套映射:通过 ms.isHasNestedResultMaps() 识别是否需要处理嵌套结果(如 association);
  • 设置结果集类型:通过 ms.getResultSetType() 配置 ResultSet 的滚动方式。
协作源码片段(DefaultResultSetHandler 类)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
// 1. 获取第一个 ResultSet
ResultSetWrapper rsw = getFirstResultSet(stmt);
// 2. 从 MappedStatement 获取 resultMaps(结果映射规则)
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
// 3. 校验结果集数量与 resultMap 数量匹配
validateResultMapsCount(rsw, resultMapCount);
// 4. 根据 resultMap 映射结果集
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
resultSetCount++;
}
return collapseSingleResultList(multipleResults);
}

实战:MappedStatement 的常见问题与优化

1. 问题 1:“Invalid bound statement (not found)” 异常

  • 原因:执行 SQL 时,Configuration 中未找到对应的 MappedStatementnamespace.id 不存在);
  • 解决方案:
    1. 检查 Mapper XML 的 namespace 与接口全限定名一致(如 com.example.mapper.UserMapper);
    2. 检查 SQL 节点的 id 与接口方法名一致(如 selectUserById);
    3. 检查 MyBatis 配置是否扫描了该 Mapper(如 <mappers> 标签配置 packageresource)。

2. 问题 2:二级缓存不生效

  • 原因MappedStatementuseCachefalsecache 未配置;
  • 解决方案:
    1. 在 Mapper XML 中添加 <cache> 标签(开启二级缓存);
    2. 确保 SQL 节点的 useCache="true"(SELECT 语句默认 true,无需显式配置);
    3. 检查 flushCacheRequired 是否为 true(若为 true,执行后会清空缓存,导致下次查询不命中)。

3. 优化:动态 SQL 与 SqlSource 性能

  • 问题:复杂动态 SQL(如大量 <if>/<foreach>)会导致 SqlSource 解析耗时;
  • 优化方案:
    1. 简化动态 SQL 逻辑,避免过度嵌套;
    2. 使用 StaticSqlSource(静态 SQL)替代 DynamicSqlSource(动态 SQL),适用于无动态逻辑的场景;
    3. 开启 MyBatis 缓存(一级 / 二级),减少重复解析与执行。

总结:MappedStatement 的核心价值

MappedStatement 是 MyBatis 中 “配置与执行的桥梁”,其核心价值体现在:

  1. 配置集中化:将分散的 SQL 节点配置聚合为一个对象,便于组件间复用与传递;
  2. 元数据标准化:为所有核心组件提供统一的元数据接口,确保执行逻辑的一致性;
  3. 扩展灵活性:支持自定义 SqlSourceLanguageDriver 等,可扩展 SQL 解析与执行逻辑;
  4. 错误可追溯:通过 resourceid 等属性,快速定位 SQL 执行错误的源头

欢迎关注我的其它发布渠道