我本来想继续写ParameterHandler,在梳理思路时发现,虽然前面写了几篇关于接口的源码解读,但是对于其中的一些概念还欠缺一下认识,尤其是MappedStatement,它贯穿了SqlSession、Executor、StatementHandler等,但是它究竟是什么、有什么用,没有一个全面的了解。所以我想先把这类内容梳理一下,把基础打牢。开始进入正题……
重新认识MappedStatement
每个MappedStatement对应了我们自定义Mapper接口中的一个方法,它保存了开发人员编写的SQL语句、参数结构、返回值结构、Mybatis对它的处理方式的配置等细节要素,是对一个SQL命令是什么、执行方式的完整定义。可以说,有了它Mybatis就知道如何去调度四大组件顺利的完成用户请求。
MappedStatement保存在Configuration#mappedStatements这个Map类型的对象中,其存储的key为MappedStatement#id,所以MappedStatement的id是不能重复的,这个id是由Mapper接口的完全限定名和方法名称拼接而成,这就导致了我们在同一个Mapper中不能出现重载的接口方法。
按照Mybatis的规范,每个Mapper方法也会对应xml中一个select/insert/update/delete标签,Mybatis为这些标签设计了一些属性,允许我们开发人员修改Mybatis的运行方式或行为。大部分情况下,我们不会关注这些属性,是因为Mybatis为其设计了默认值,方便我们开箱即用。我把MappedStatement类中的主要字段进行了注释,这些字段都可以找到与之对应的标签属性,可参考《XML映射文件》,代码贴在了下面,大家先对MappedStatement有个大概的认识。
1public final class MappedStatement {
2 /**
3 * 对应所属mapper的资源路径,如我们示例中的CompanyMapper.xml
4 */
5 private String resource;
6
7 /**
8 * mybatis全局的配置对象
9 */
10 private Configuration configuration;
11
12 /**
13 * 当前MappedStatement的唯一识别ID,并且在同一个Configuration中是唯一的
14 * 它由Mapper类的完全限定名和Mapper方法名称拼接而成
15 */
16 private String id;
17
18 /**
19 * mybatis每次从数据库中返回记录的大小,通过对该值的优化,可以提升查询效率
20 */
21 private Integer fetchSize;
22
23 /**
24 * 当前MappedStatement执行时,数据库操作的超时时间
25 */
26 private Integer timeout;
27
28 /**
29 * SQL声明的类型,决定当前MappedStatement由哪种类型的StatementHandler执行
30 * StatementType枚举有:STATEMENT, PREPARED, CALLABLE
31 * 默认值是:PREPARED
32 */
33 private StatementType statementType;
34
35 /**
36 * 结果集处理类型,决定了结果集游标的移动方式:
37 * 只能向前移动、双向移动且对修改敏感、双向移动对修改不敏感。
38 */
39 private ResultSetType resultSetType;
40
41 /**
42 * 存储我们定义的经过mybatis初步解析处理的sql语句,由若干sql节点构成,包含一些动态节点,如If条件语句。
43 * 在生成SqlSource之前,已经把<include></include>标签的内容转为了实际的文本对象
44 */
45 private SqlSource sqlSource;
46
47 /**
48 * 二级缓存策略配置对象
49 */
50 private Cache cache;
51
52 /**
53 * 参数映射,外部以何种形式对当前MappedStatement传参
54 */
55 private ParameterMap parameterMap;
56
57 /**
58 * 结果映射列表,应该是只有一个的,不明白为啥是列表,可能是多个结果集返回时使用的。
59 */
60 private List<ResultMap> resultMaps;
61
62 /**
63 * 是否要刷新缓存,将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空
64 * 对select命令,默认值为false,对insert、update、delete默认为true。
65 */
66 private boolean flushCacheRequired;
67
68 /**
69 * 将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。
70 */
71 private boolean useCache;
72
73 /**
74 * sql命令类型:如select、update、insert、delete等
75 */
76 private SqlCommandType sqlCommandType;
77
78 //……
79
80 /**
81 * 语言驱动,如xml。
82 */
83 private LanguageDriver lang;
84
85 /**
86 * 结果集类型列表
87 */
88 private String[] resultSets;
89}
MappedStatement是怎么来的?
还是以XML配置方式为例进行分析,简单说下源码查找的过程。Mapper对应的SQL语句定义在xml文件中,顺着源码会发现完成xml解析工作的是XMLMapperBuilder,其中对xml中“select|insert|update|delete”类型元素的解析方法为buildStatementFromContext;buildStatementFromContext使用了XMLStatementBuilder类对statement进行解析,并最终创建了MappedStatement。
所以,XMLStatementBuilder#parseStatementNode方法就是我们分析的重点。但是,在此之前需要有一点准备工作要做。由于MappedStatement最终是由MapperBuilderAssistant构建的,它其中存储了一些Mapper级别的共享信息并应用到MappedStatement中。所以,先来简单了解下它的由来:
1public class XMLMapperBuilder extends BaseBuilder {
2 private final XPathParser parser;
3 private final MapperBuilderAssistant builderAssistant;
4
5 //省略部分字段和方法
6
7 public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
8 this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
9 configuration, resource, sqlFragments);
10 }
11
12 private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
13 super(configuration);
14 this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
15 this.parser = parser;
16 this.sqlFragments = sqlFragments;
17 this.resource = resource;
18 }
19
20 //省略部分字段和方法
21
22 private void configurationElement(XNode context) {
23 try {
24 String namespace = context.getStringAttribute("namespace");
25 if (namespace == null || namespace.equals("")) {
26 throw new BuilderException("Mapper's namespace cannot be empty");
27 }
28 builderAssistant.setCurrentNamespace(namespace);
29 //省略部分代码
30 buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
31 } catch (Exception e) {
32 throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
33 }
34 }
-
代码14行:MapperBuilderAssistant由XMLMapperBuilder的私有构造方法创建,这里传入了全局Configuration对象和xml资源文件路径;
-
代码24~28行:设置builderAssistant的namespace。这个namespace就是我们在mapper xml中声明的,也就是我们Mapper接口的完全限定名,如com.raysonxin.dao.CompanyDao。
-
代码30行:开始了声明节点的解析。
好了,下面正是开始XMLStatementBuilder#parseStatementNode的分析了。为了节省篇幅,我直接通过代码注释的方式进行说明了,部分我认为不关键或不常用的内容没有多说。
1 /**
2 * parseStatementNode方法是对select、insert、update、delete这四类元素进行解析,大体分为三个过程:
3 * 1、解析节点属性:如我们最常用的id、resultMap等;
4 * 2、解析节点内的sql语句:首先把sql语句中包含的<include></include>等标签转为实际的sql语句,然后执行静态或动态节点处理;
5 * 3、根据以上解析到的内容,使用builderAssistant创建MappedStatement,并加入Configuration中。
6 * <p>
7 * 以上过程中最关键的是第二步,它会根据实际使用的标签,把sql片段转为不同的SqlNode,以链表方式存储到SqlSource中。
8 */
9 public void parseStatementNode() {
10
11 //获取标签的id属性,如selectById,对应Mapper接口中的方法名称
12 String id = context.getStringAttribute("id");
13 //获取databaseId属性,我们一般都没有写。
14 String databaseId = context.getStringAttribute("databaseId");
15
16 /**
17 * 这段代码虽然不起眼,但是一定要进去看一下:其内部完成了对id的再次赋值,
18 * 处理的方式是:id=namespace+"."+id,也就是当前Mapper的完全限定名+"."+id,
19 * 比如我们之前例子中的com.raysonxin.dao.CompanyDao.selectById
20 * 这也是Mapper接口中不能存在重载方法的根本原因。
21 * */
22 if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
23 return;
24 }
25
26 /**
27 * 下面这块代码会依次获取fetchSize、timeout、resultMap等属性,
28 * 需要注意的是,有些属性虽然我们没有设置,但是mybatis会设置默认值,
29 * 具体可以查看mybatis的官方说明。
30 */
31 Integer fetchSize = context.getIntAttribute("fetchSize");
32 Integer timeout = context.getIntAttribute("timeout");
33 String parameterMap = context.getStringAttribute("parameterMap");
34 String parameterType = context.getStringAttribute("parameterType");
35 Class<?> parameterTypeClass = resolveClass(parameterType);
36 String resultMap = context.getStringAttribute("resultMap");
37 String resultType = context.getStringAttribute("resultType");
38 String lang = context.getStringAttribute("lang");
39 //默认值:XMLLanguageDriver
40 LanguageDriver langDriver = getLanguageDriver(lang);
41
42 Class<?> resultTypeClass = resolveClass(resultType);
43 String resultSetType = context.getStringAttribute("resultSetType");
44 //默认值:PREPARED
45 StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
46 //默认值:DEFAULT
47 ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
48
49 String nodeName = context.getNode().getNodeName();
50 SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
51 boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
52 boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
53 boolean useCache = context.getBooleanAttribute("useCache", isSelect);
54 boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
55
56 // Include Fragments before parsing
57 /**
58 * 英文注释也说了,在sql解析前处理 include 标签,比如说,我们include了BaseColumns,
59 * 它会把这个include标签替换为BaseColumns内的sql内容
60 * */
61 XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
62 includeParser.applyIncludes(context.getNode());
63
64 // Parse selectKey after includes and remove them.
65 //处理selectKey,主要针对不同的数据库引擎做处理
66 processSelectKeyNodes(id, parameterTypeClass, langDriver);
67
68 // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
69 /**
70 * 到了关键步骤了:就是通过这句代码完成了从xml标签到SqlSource的转换,
71 * SqlSource是一个接口,这里返回的可能是DynamicSqlSource、也可能是RawSqlSource,
72 * 取决于xml标签中是否包含动态元素,比如 <if test=""></if>
73 * */
74 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
75 String resultSets = context.getStringAttribute("resultSets");
76
77 //下面这些是针对selectKey、KeyGenerator等进行处理,暂时跳过了。
78 String keyProperty = context.getStringAttribute("keyProperty");
79 String keyColumn = context.getStringAttribute("keyColumn");
80 KeyGenerator keyGenerator;
81 String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
82 keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
83 if (configuration.hasKeyGenerator(keyStatementId)) {
84 keyGenerator = configuration.getKeyGenerator(keyStatementId);
85 } else {
86 keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
87 configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
88 ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
89 }
90
91 /**
92 * 节点及属性都解析完成了,使用builderAssistant创建MappedStatement,
93 * 并保存到Configuration#mappedStatements。
94 * */
95 builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
96 fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
97 resultSetTypeEnum, flushCache, useCache, resultOrdered,
98 keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
99 }
我们在xml中定义的select等语句就是通过这个parseStatementNode方法解析为MappedStatement的,整体来看比较容易理解,核心就是SqlSource的创建过程,大家可以写个简单的例子一步一步调试看下。
SqlSource是什么,如何创建的?
SqlSource是整个MappedStatement的核心,MappedStatement其他一大堆字段都是为了准确的执行它而定义的。SqlSource是个半成品的sql语句,因为对于其中的动态标签还没静态化,其中的参数也未赋值。正是如此,才为我们后续的调用执行提供了基础,接下来重点看看SqlSource的构建过程。为了先从整体上了解,我画了一个时序图来描述SqlSource的解析、创建过程。

这个过程涉及三个参与者XMLStatementBuilder、XMLLanguageDriver、XMLScriptBuilder,核心在于XMLScriptBuilder对标签的识别与解析,我们重点看XMLScriptBuilder#parseScriptNode这个方法,如下:
1 /**
2 * parseScriptNode字面意思,解析sql脚本节点。
3 * */
4 public SqlSource parseScriptNode() {
5 // parseDynamicTags处理节点中的动态标签,其实动态、静态标签都会读取,
6 // 只是对于动态标签会使用对应的动态标签处理器解析。
7 MixedSqlNode rootSqlNode = parseDynamicTags(context);
8 SqlSource sqlSource = null;
9
10 // 如果是动态的,创建DynamicSqlSource;否则创建RawSqlSource。
11 // isDynamic默认是false,parseDynamicTags处理中,只要存在动态元素,他就被置为true
12 if (isDynamic) {
13 sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
14 } else {
15 sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
16 }
17 return sqlSource;
18 }
刚才一直再说SqlNode,那SqlNode到底是什么呢?结合一个例子,我们先简单认识一下它,同样放一张类图(图中并没有放全)来了解其家族。

1<select id="selectById" resultMap="baseResultMap" >
2 select
3 <include refid="BaseColumns"></include>
4 from company
5 <if test="id != null">
6 where id= #{id}
7 </if>
8</select>
对于这个示例中的select标签的sql语句(只看2-7行),mybatis会按照标签完整性(闭合)解析为多个节点,同时根据节点中出现的元素类型创建不同类型的节点(依据是《Document Object Model (DOM) Level 3 Core Specification》中定义的12中类型),比如例子中,纯文本解析为TextSqlNode,if标签解析为IfSqlNode。
但是,我们在编写sql语句时大多数情况是多种类型混合的,所以就有了MixedSqlNode,它以List
1 protected MixedSqlNode parseDynamicTags(XNode node) {
2 List<SqlNode> contents = new ArrayList<>();
3 NodeList children = node.getNode().getChildNodes();
4 //遍历节点
5 for (int i = 0; i < children.getLength(); i++) {
6 //获取当前节点
7 XNode child = node.newXNode(children.item(i));
8 //判断节点类型是否为CDATA_SECTION_NODE或TEXT_NODE
9 if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
10 //获取节点的sql语句内容
11 String data = child.getStringBody("");
12 //创建TextSqlNode
13 TextSqlNode textSqlNode = new TextSqlNode(data);
14 //判断是否为动态节点,实际判断是否包含${}占位符,有就是true
15 if (textSqlNode.isDynamic()) {
16 contents.add(textSqlNode);
17 //设置为动态sql
18 isDynamic = true;
19 } else {
20 //不是动态节点,创建StaticTextSqlNode
21 contents.add(new StaticTextSqlNode(data));
22 }
23 }
24 //节点类型是否为ELEMENT_NODE
25 else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
26 //获取节点的名称,比如if
27 String nodeName = child.getNode().getNodeName();
28 //获取对应的处理器,如IfHandler
29 XMLScriptBuilder.NodeHandler handler = nodeHandlerMap.get(nodeName);
30 if (handler == null) {
31 throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
32 }
33 //调用处理器,解析动态节点内容,这里面也会递归调用parseDynamicTags,逐层处理。
34 handler.handleNode(child, contents);
35 //设置为动态sql
36 isDynamic = true;
37 }
38 }
39 //创建MixedSqlNode
40 return new MixedSqlNode(contents);
41 }
总结一下parseDynamicTags方法的处理过程,parseDynamicTags仅处理类型为CDATA_SECTION_NODE、TEXT_NODE、ELEMENT_NODE的节点。
-
前两种类型会首先作为TextSqlNode,若节点中没有占位符
${}
,则转为StaticTextSqlNode;若节点中有占位符${}
,节点类型为TextSqlNode不变,但是会将这条sql设置为dynamic。这会导致后续的参数设置方式不同,引出#{}
、${}
的差别,我们在下一节在说明。 -
对ELEMENT_NODE节点,会根据节点名称(if、choose等)找到对应的处理器解析为动态节点,处理器有IfHandler、WhereHandler等9种。
所以,回到XMLScriptBuilder#parseScriptNode方法,根据isDynamic的值,会创建不同类型的SqlSource。到目前为止,我们知道创建DynamicSqlSource有两种情况:一是sql节点包含if、choose、where这里动态标签时;二是使用了${}
占位符时。其余情况会创建RawSqlSource。
好了,SqlSource的创建过程我们就分析完了,在翻上去看看那张时序图加深一下印象吧。
SqlSource创建完成后,就剩下builderAssistant#addMappedStatement这个过程了,比较简单,大家可以自己查看源码了解一下,我就不废话了。
本文总结
正如文章开头所说,本文的主要目的是打基础,弄清楚是什么,为什么,为以后的怎么做做好铺垫。本文分析了MappedStatement主要字段及作用、Mybatis如何创建MappedStatement、MappedStatement中的SqlSource是什么及创建过程几个问题,虽然写的很啰嗦,但是问题应该是描述的差不多了,希望能对大家的理解有一些帮助。
本文到这里就结束了,希望对您有用,如果觉得有用就点个赞吧,^_^!本人水平有限,如您发现有任何错误或不当之处,欢迎批评指正。也可以关注我的微信公众号:“兮一昂吧”。
原文始发于微信公众号(码路印记):Mybatis源码之MappedStatement
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/23787.html