以前只知道select标签的个别属性用法,并不了解其实现流程与原理,正巧,最近在学习Cache一二级缓存,看到与select标签相关的属性,就借此机会把这篇文章写了出来。
此外,之所以以select为视角,是因为其他几个标签的源码流程与select大致相当,而且select在实际开发中使用更为频繁。
本文思路:
1. 先了解select标签各个属性作用
2. 从mapper.xml中加载select标签的过程
3. 加载sql操作标签的重要类---MappedStatement
4. 以select为例,解析select在执行时的流程,以及哪些属性将会影响执行中的select查询
5. 什么情况下会创建新的SqlSession,以及何时共用同一SqlSession。
6. SqlSession执行select操作的事务提交操作
涉及注解:
@Select
@Results
@Result
@CacheNamespace
@TypeDiscriminator
Mybatis官网中展示了如下属性
每个属性的作用(版本mybatis3)
Attribute | Description |
---|---|
id |
当前namespace中代表该条语句的唯一id。 |
parameterType |
将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler)推断出具体传入语句的参数,默认值为未设置(unset)。 |
parameterMap |
|
resultType |
期望从这条语句中返回结果的类全限定名或别名。如果需要返回集合,那么这里的类型应该是集合的元素的类型,而不是集合本身的类型。resultType 和 resultMap 之间只能同时使用一个。 |
resultMap |
引用外部resultMap标签(结果映射)的id。 |
flushCache |
为true时将会在调用此语句时清空一级缓存(本地缓存)和二级缓存。默认false。 |
useCache |
为true时将查询语句的结果缓存在二级缓存中。默认true。 |
timeout |
设置数据库连接超时等待时间。默认未设置(由驱动driver程序控制超时时间)。 |
fetchSize |
这是一个给驱动的建议值,尝试让驱动程序每次批量返回的结果行数等于这个设置值。 默认值为未设置(unset)(依赖驱动)。 |
statementType |
执行SQL语句的类型。默认值:PREPARED 。可选项:STATEMENT、 PREPARED、 CALLABLE 。依据上述顺序MyBatis分别使用Statement(执行静态SQL语句) , PreparedStatement(执行预编译SQL语句) 或CallableStatement(执行SQL存储过程)来进行语句操作 。 |
resultSetType |
可选项:FORWARD_ONLY、 SCROLL_SENSITIVE、 SCROLL_INSENSITIVE、 DEFAULT (与未设置相同)。默认未设置。 |
databaseId |
如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句;如果带和不带的语句都有,则不带的会被忽略。 |
resultOrdered |
这个设置仅针对嵌套结果 select 语句:如果为 true,将会假设包含了嵌套结果集或是分组,当返回一个主结果行时,就不会产生对前面结果集的引用。 这就使得在获取嵌套结果集的时候不至于内存不够用。默认值:false 。 |
resultSets |
这个设置仅适用于多结果集的情况。它将列出语句执行后返回的结果集并赋予每个结果集一个名称,多个名称之间以逗号分隔。 |
上面就是select标签中所有的属性,在后面的加载流程中将只会涉及常用的属性。
那么在mybatis源码中,执行xml
从总体上看,大约有两个步骤:
1. 创建MappedStatement
2. 创建会话(SqlSession)并执行SQL。
下面我会主要围绕这两个大步骤来学习Mybatis框架。
在mybatis代码中,每个xml操作数据库的标签(
在Mybatis提交SQL语句到数据库时,MappedStatement类就包含了执行操作所需要的预编译SQL语句、参数配置、状态信息、缓存等信息。
注意:这里的SqlSessionFactoryBean类,是Spring集成mybatis的包,用于Spring管理mybatis生成的bean,其他集成框架如mybatis-plus可以与这里有所不同。参考MyBatis-Spring。
Mybatis中有两种方法生成MappedStatement:
1. 注解方式(MapperAnnotationBuilder)
2. xml方式(XMLMapperBuilder)
前者通过扫描Mapper.java文件读取注解、参数等相关内容,后者扫描Mapper.xml文件读取xml标签的内容。这二者在生成时都会尝试解析对方的文件是否被扫描,并尝试对其进行扫描。
这里不会去深究这两种方式是如何读取的文件内容,只会从总体上了解他们的处理流程。由于时间不足,本文只讨论通过xml方式生成MappedStatement,只会在一些必要场景处涉及注解生成。
如下,是mapper.xml文件中的一个简单的select标签
每一个xml操作标签通过一系列操作后转为与之对应的MappedStatement类,select标签中的每个属性都被赋值给了MappedStatement类中的属性。
public final class MappedStatement {
private String resource;
private Configuration configuration; // 全局配置环境
private String id;
private Integer fetchSize;
private Integer timeout;
private StatementType statementType; // 根据sql类型选择不同的sql处理方式
private ResultSetType resultSetType;
private SqlSource sqlSource; // 处理后的sql语句
private Cache cache;
private ParameterMap parameterMap;
private List resultMaps;
private boolean flushCacheRequired;
private boolean useCache;
private boolean resultOrdered;
private SqlCommandType sqlCommandType;
private KeyGenerator keyGenerator;
private String[] keyProperties;
private String[] keyColumns;
private boolean hasNestedResultMaps;
private String databaseId;
private Log statementLog;
private LanguageDriver lang;
private String[] resultSets;
}
MappedStatement类由内部类Builder使用建造者模式构建,Builder的构造函数中展示了一些属性的默认设置以及初始化情况。
public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
mappedStatement.configuration = configuration;
mappedStatement.id = id;
mappedStatement.sqlSource = sqlSource;
mappedStatement.statementType = StatementType.PREPARED;
mappedStatement.resultSetType = ResultSetType.DEFAULT;
mappedStatement.parameterMap = new ParameterMap.Builder(configuration, "defaultParameterMap", null, new ArrayList<>()).build();
mappedStatement.resultMaps = new ArrayList<>();
mappedStatement.sqlCommandType = sqlCommandType;
mappedStatement.keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
String logId = id;
if (configuration.getLogPrefix() != null) {
logId = configuration.getLogPrefix() + id;
}
mappedStatement.statementLog = LogFactory.getLog(logId);
mappedStatement.lang = configuration.getDefaultScriptingLanguageInstance();
}
Builder构造函数实际传入的参数:
按照上面的流程图,进行源码学习、分析
这个类位于mybatis与spring集成的包中。
方法的主要功能是扫描指定位置的所有mapper.xml文件,并通过调用XMLMapperBuilder类处理这些xml文件。
for (Resource mapperLocation : this.mapperLocations) {
if (mapperLocation == null) {
continue;
}
try {
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
xmlMapperBuilder.parse();
} catch (Exception e) {
throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
} finally {
ErrorContext.instance().reset();
}
LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
}
除了解析xml文件内容外,还会通过bindMapperForNamespace()方法对对应的Mapper.java接口进行扫描。
这一步有防重复扫描验证,每次扫描完文件后,都会将其存放在全局配置中的已加载资源中。
public void parse() {
if (!configuration.isResourceLoaded(resource)) { // 避免二次扫描
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource); //添加至已扫描列表
bindMapperForNamespace(); // 将Mapper.java接口的注解等内容绑定至同一命名空间
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
bindMapperForNamespace方法最终调用MapperAnnotationBuilder#parse,其扫描过程与xml方式的一致,它会通过Method的方法获取注解、传入参数、返回类型等数据,从而获得来预编译SQL语句、参数配置、状态信息、缓存等信息。
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
parseCache(); // @CacheNamespace
parseCacheRef(); // @CacheNamespaceRef
for (Method method : type.getMethods()) {
if (!canHaveStatement(method)) {
continue;
}
if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
&& method.getAnnotation(ResultMap.class) == null) {
parseResultMap(method); // 获取结果映射
}
try {
parseStatement(method); // 生成MappedStatement
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
例如parseResultMap(method)方法,平时我们写的@Results、@Result、@TypeDiscriminator等注解就是通过下面的parseResultMap(method)方法解析的。这里就不一一介绍了。
private String parseResultMap(Method method) {
Class> returnType = getReturnType(method);
Arg[] args = method.getAnnotationsByType(Arg.class);
Result[] results = method.getAnnotationsByType(Result.class);
TypeDiscriminator typeDiscriminator = method.getAnnotation(TypeDiscriminator.class);
String resultMapId = generateResultMapName(method);
applyResultMap(resultMapId, returnType, args, results, typeDiscriminator); // 生成指定的映射类以及与每个sql字段对应的类的属性
return resultMapId;
}
configurationElement()方法是解析xml文件各个标签元素的主要方法。正如代码中context.evalNode所展示的,它的作用就是提取对应的标签元素,并通过对应方法进行处理。
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref")); // 解析二级缓存关联的命名空间
cacheElement(context.evalNode("cache")); // 解析是否打开二级缓存,打开时解析二级缓存的属性
parameterMapElement(context.evalNodes("/mapper/parameterMap")); // 解析
resultMapElements(context.evalNodes("/mapper/resultMap")); // 解析标签
sqlElement(context.evalNodes("/mapper/sql")); // 解析标签
buildStatementFromContext(context.evalNodes("select|insert|update|delete")); // 增删改查标签
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
这里要提一点,大部分生成好的数据都会存放在Configuration这个全局环境中。少部分会用于生成MappedStatement的中间数据,例如
标签 | 作用 | 存放类 | 是否被final修饰 | 属性名 |
cache-ref | 二级缓存关联命名空间 | Configuration | true | cacheRefMap |
cache | 二级缓存 | Configuration | true | caches |
cache | 二级缓存 | MapperBuilderAssistant | false | currentCache |
parameterMap | 入参 | Configuration | true | parameterMaps |
resultMap | 返回结果 | Configuration | true | resultMaps |
sql | Configuration | true | sqlFragments | |
sql | XMLMapperBuilder | true | sqlFragments | |
select|insert|update|delete | sql语句的配置信息 | Configuration | true | mappedStatements |
遍历该命名空间中的每一个SQL标签(select|insert|update|delete)。
private void buildStatementFromContext(List list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
其中每一个XNode context的数据如下:
这个方法是最终解析
1.
一些不需要处理直接获取的属性,下面代码可以看到flushCache、useCache、parameterType等属性的解析过程。
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
………………
这些属性是通过key value的方式获取的,通过预先设定的类型转换为需要值,当值为空时,设置默认值。
譬如context.getBooleanAttribute("flushCache", !isSelect),通过这个方法获取Boolean类型的flushCache值,如果该属性没有在
2.
解析所有
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
找到include标签的节点,并将其替换掉。
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
// 遍历include类型的元素节点
if ("include".equals(source.getNodeName())) {
Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext); // 获取include的refid属性值,并在sqlFragments中尝试获取该sql标签
Properties toIncludeContext = getVariablesContext(source, variablesContext);
applyIncludes(toInclude, toIncludeContext, true); // include中的include标签
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude = source.getOwnerDocument().importNode(toInclude, true);
}
source.getParentNode().replaceChild(toInclude, source); // include标签替换为sql标签
while (toInclude.hasChildNodes()) {
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
}
toInclude.getParentNode().removeChild(toInclude);
} else if (source.getNodeType() == Node.ELEMENT_NODE) {
// 如果当前节点是一个元素节点,继续向下遍历这个元素的每一个节点
if (included && !variablesContext.isEmpty()) {
// replace variables in attribute values
NamedNodeMap attributes = source.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Node attr = attributes.item(i);
attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
}
}
NodeList children = source.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
applyIncludes(children.item(i), variablesContext, included);
}
} else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
&& !variablesContext.isEmpty()) {
// replace variables in text node 替换Text类型节点中的变量
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
}
这里可以简单的介绍一下Mybatis使用的Node以及实现类DeferredXXXXImpl。
Node接口是整个文档对象模型的主要数据类型。 它代表文档树中的单个节点,每一个节点都定义了各自的数据类型。
而由于每个节点的类型不同,也就有了处理各种节点类型的实现类。·
这些类中主要属性的作用:
/** 当前节点树的第一个节点 */
protected ChildNode firstChild = null;
/** 前一个节点 */
protected ChildNode previousSibling;
/** 后一个节点 */
protected ChildNode nextSibling;
/** 节点名称 */
protected String name;
/** 标签的属性Map */
protected AttributeMap attributes;
可以看看下面这一段
指明下面的node是一个"select"类型的标签元素。DeferredElementImpl代表当前节点是一个XML文档的元素节点,其下面的所有节点的ownerNode都指向它本身。
firstChild是这个标签元素的第一个节点,它是一个DeferredTextImpl类型的节点(文本节点保存元素或属性的非标记、非实体内容)。
第二节点是一个"include"类型的标签元素,同样是DeferredElementImpl。
…………
3. parameterType、resultType属性的获取
有关别名转换为真实类型或@Alias注解的内容可以参考这一篇文章:【Mybatis源码解析】 parameterType通过别名或缺省方式找到真实类型
String parameterType = context.getStringAttribute("parameterType");
Class> parameterTypeClass = resolveClass(parameterType);
…………
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
4. SqlSource的生成
xml中的动态sql语言是通过该段解析。这些动态的sql语言的解析过程,等日后有时间再看………
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context); // 将刚刚的DeferredXXXXImpl解析为MixedSqlNode
SqlSource sqlSource;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
直接看SqlSource的生成结果,与我们在数据库执行的SQL已经相差无几了:
最终将把参数通过MappedStatement的建造者生成MappedStatement。
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
到这里MappedStatement生成完毕。这一步的参数,都是通过上面的步骤获取的,需要提一点的是,如果有二级缓存,那么这里的currentCache是公用属于同一个命名空间的缓存。
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache); // 二级缓存
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
至此经过mapper接口、xml文件的解析之后,就转换为了Mybatis增删改查时需要的MappedStatement类了,下一步要研究的就是查询时如何创建Sqlsession以及执行时如何使用ms类。
待续…………