您的位置:首页 > 编程语言

Mybatis 代码流程及实现原理解析(三)

2013-08-21 14:56 211 查看
接上篇, 这篇继续分析XMLMapperBuilder.parse()里的configurationElement() 这个方法。

private void configurationElement(XNode context) {
try {
//mapper可以加namespace来避免重复情况
String namespace = context.getStringAttribute("namespace");
builderAssistant.setCurrentNamespace(namespace);
//该节点表示从其他名目空间引用缓存配置
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));//解析cache子节点.
//解析parameterMap 子节点,因为有多个并列的parameterMap节点,所以要加上路径,解析返回的列表
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//解析resultMap子节点,因为有多个并列的resultMap节点,所以要加上路径,解析返回的列表
resultMapElements(context.evalNodes("/mapper/resultMap"));
//解析sql子节点,因为有多个并列的sql节点,所以要加上路径,解析返回的列表
sqlElement(context.evalNodes("/mapper/sql"));
//解析select,insert,update,delete子节点
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new RuntimeException("Error parsing Mapper XML. Cause: " + e, e);
}
}


此方法逐步解析<mapper>的子节点。

子节点<cache-ref>

xml配置片段:

<cache-ref namespace="com.Book"/>


java代码解析:

private void cacheRefElement(XNode context) {
if (context != null) {
//首先用该节点的namespace替换cacherefmap中mapper文件的。
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
//创建resolver,判断该namespace是否可用。
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
//如果不可用,加入到为完成列表,在所有节点解析完成后,再统一做处理
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}
CacheRefResolver.java的resolve方法

public Cache resolveCacheRef() {
return assistant.useCacheRef(cacheRefNamespace);
}
MapperBuilderAssistant.java的useCacheRef方法:

public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true;
Cache cache = configuration.getCache(namespace);
//如果当前configuration对象没有持有该namespace的Cache, 则抛出异常。
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
currentCache = cache;
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}


子节点<cache>

xml配置如下:

<mapper namespace="com.skoyou.domain.UserDAO">
<cache type="com.domain.something.MyCustomCache"
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="tr">
<property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
</cache>
type:配置自定义缓存或为其他第三方缓存方案 创建适配器来完全覆盖缓存行为。如果未指定的话, 使用“PERPETFUL”类型(PerpetualCache.java)。

eviction:缓存回收策略,默认值是“LRU”, 有如下几种策略可以指定:

LRU:最近最少使用的:移除最长时间不被使用的对象。

FIFO:先进先出:按对象进入缓存的顺序来移除它们。

SOFT:软引用:移除基于垃圾回收器状态和软引用规则的对象。

WEAK:弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象.

flushInterval:刷新间隔)可以被设置为任意的正整数,而且它们代表一个合理的毫秒 形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。

size:(引用数目)可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的 可用内存资源数目。默认值是 1024。

readyOnly:(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓 存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存 会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全,因此默认是 false。

cache可以有个子节点<property> 来加载.

java方法解析:

private void cacheElement(XNode context) throws Exception {
if (context != null) {
//获取cache type的值,没有的话,使用默认PERPETUAL
String type = context.getStringAttribute("type", "PERPETUAL");
// 得到type类, 默认是PERPETUAL 对应的PerpetualCache
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
//获取eviction的值,没有的话,使用默认LRU得到eviction类, 即LruCache.class
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
Properties props = context.getChildrenAsProperties();
//根据提供的参数, 建一个cache对象, 并将这个对象放入到Configuration 对象中.
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props);
}
}


子节点<parameterMap>

在Mybatis中,这个节点已经被废弃了!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除。其实解析还是比较简单的。取这个节点的所有属性,build出一个ParameterMapping对象,方法parametermappings列表里, 然后将这个列表更新到configuration对象中的ParameterMap中去。

for (XNode parameterMapNode : list) {
String id = parameterMapNode.getStringAttribute("id");
String type = parameterMapNode.getStringAttribute("type");
Class<?> parameterClass = resolveClass(type);
List<XNode> parameterNodes = parameterMapNode.evalNodes("parameter");
List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
for (XNode parameterNode : parameterNodes) {
String property = parameterNode.getStringAttribute("property");
String javaType = parameterNode.getStringAttribute("javaType");
String jdbcType = parameterNode.getStringAttribute("jdbcType");
String resultMap = parameterNode.getStringAttribute("resultMap");
String mode = parameterNode.getStringAttribute("mode");
String typeHandler = parameterNode.getStringAttribute("typeHandler");
Integer numericScale = parameterNode.getIntAttribute("numericScale");
ParameterMode modeEnum = resolveParameterMode(mode);
Class<?> javaTypeClass = resolveClass(javaType);
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
@SuppressWarnings("unchecked")
Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
ParameterMapping parameterMapping = builderAssistant.buildParameterMapping(parameterClass, property, javaTypeClass, jdbcTypeEnum, resultMap, modeEnum, typeHandlerClass, numericScale);
parameterMappings.add(parameterMapping);
}
builderAssistant.addParameterMap(id, parameterClass, parameterMappings);
}
}


子节点<resultMap>

这个节点的解析较复杂,会在下一遍中单独讲解。

子节点<sql>

<sql id="userColumns"> id,username,password </sql>
这个元素可以被用来定义可重用的 SQL 代码段,可以包含在其他语句中。

Java 代码

private void sqlElement(List<XNode> list) throws Exception {
if (configuration.getDatabaseId() != null) {  //传人configuration现有的database id。
sqlElement(list, configuration.getDatabaseId());
}
sqlElement(list, null);
}
sqlElement方法:

private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
for (XNode context : list) { //由于有多个并列的sql节点,需求逐个解析。
//sql 也可以有自己的database id,但实际上这个id要与全局配置的一致
String databaseId = context.getStringAttribute("databaseId");
String id = context.getStringAttribute("id");
//判断id是否包含有当前的namespace,包含的话直接返回当前id。 没有的话,则以当前mapper的namespace.id值返回
id = builderAssistant.applyCurrentNamespace(id, false);
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId))
sqlFragments.put(id, context);
}
}


databaseIdMatchesCurrent 方法:

sql节点自己指定的database id要与configuration中的一致。

private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
if (requiredDatabaseId != null) {
if (!requiredDatabaseId.equals(databaseId)) {
return false;
}
} else {
if (databaseId != null) {
return false;
}
// skip this fragment if there is a previous one with a not null databaseId
if (this.sqlFragments.containsKey(id)) {
XNode context = this.sqlFragments.get(id);
if (context.getStringAttribute("databaseId") != null) {
return false;
}
}
}
return true;
}
实际上这个sqlFragments map是在XMLMappBuilder初始化的时候,有configuration传入的, 所以configuration能得到最新的对这个map的修改。这个map会在build select等节点的时候被用到。

子节点<select>,<insert>,<update>,<delete>

节点配置,以select为例:

<select
id="selectPerson"
databaseId="test"
fetchSize="256"
timeout="10000"
parameterMap="deprecated"
parameterType="int"
resultMap="personResultMap"
resultType="hashmap"
lang="XML"
resultSetType="FORWARD_ONLY"
statementType="PREPARED"
flushCache="false"
useCache="true"
resultOrdered="false"
>


节点属性描述:

属性描述
id在命名空间中唯一的标识符,可以被用来引用这条语句。
databaseId如果配置了databaseProvider,databaseId要与配置的一致。如果不匹配的话,后续代码不会继续解析
fetchSize这是暗示驱动程序每次批量返回的结果行数。默认不设置(驱动 自行处理)。
timeout这个设置驱动程序等待数据库返回请求结果,并抛出异常时间的 最大等待值。默认不设置(驱动自行处理)
parameterMap这是引用外部 parameterMap 的已经被废弃的方法。使用内联参数 映射和 parameterType 属性。
parameterType将会传入这条语句的参数类的完全限定名或别名。
resultMap命名引用外部的 resultMap。 返回 map 是 MyBatis 最具力量的特性, 对其有一个很好的理解的话, 许多复杂映射的情形就 能被解决了。 使用 resultMap 或 resultType,但不能同时使用。
resultType从这条语句中返回的期望类型的类的完全限定名或别名。注意集 合情形,那应该是集合可以包含的类型,而不能是集合本身。使 用 resultType 或 resultMap,但不能同时使用
lang提供”XML“或”RAW“,不同的lang,可能会影响最终的sql语句的生成,默认是”XML“
resultSetTypeFORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE 中的一种。默认是不设置(驱动自行处理)。
statementTypeSTA TEMENT,PREPARED 或 CALLABLE 的一种。 这会让 MyBatis 使用选择使用 Statement,PreparedStatement 或 CallableStatement。 默认值:PREPARED
fhushCache将其设置为 true,不论语句什么时候被调用,都会导致缓存被 清空。默认值:false
userCache将其设置为 true, 将会导致本条语句的结果被缓存。 默认值: true
resultOrdered这个属性值适用于有嵌套结果的select 语句。如果是true, 那么就认为包含了嵌套的结果, 或者嵌套的结果被分在一组。这样的话, 当一个新的主结果返回时, 就不会出现上一个结果行。 这能够更加内存友好地填充嵌套结果。默认是false。
另外还有3个是只对insert有效的属性:

useGeneratedKeys 这会告诉 MyBatis使用JDBC的getGeneratedKeys 方法来取出由数据(比如:像 MySQL 和 SQL Server 这样的数据库管理系统的自动递增字段)内部生成的主键。 默认值:false
keyProperty 标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值。 默认: 不设置
keyColumn标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值。 默认: 不设置
sql语句的解析实际上是在 XMLStatementBuider的parseStatementNode方法中解析的。

private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
//构造statementBuilder。这几个sql语句相关的节点在这个类里解析
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}


XMLStatementBuilder.java的 parseStatementNode方法,如下。这个方法比较长,但主要是读取属性值,然后包装生成一个MappedStatement,并放入configuration对象中。

public void parseStatementNode() {
//读取id 属性
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
//读取databaseId 属性,但是一定要与现有存在的匹配, 否则不继续解析剩余节点
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) return;

Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);

Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
//statementType 默认是PreparedStatement
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//如果是select语句,则默认值是false。否则默认值是true。
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
//如果是select语句,则默认值是true。否则默认值是false。
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

// 解析<include>子节点,其实是用引用替换<sql>节点的实际内容。
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());

// 解析<selectKey>子节点,仅对<insert>有用。后面会讲解parseSelectKeyNodes方法
List<XNode> selectKeyNodes = context.evalNodes("selectKey");
if (configuration.getDatabaseId() != null) {
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
}
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);

//默认是XMLLanguageDriver,parameterTypeClass用不上
//生成持有当前sql语句的sqlSource对象,后面会具体看这个sqlSource是怎么生成的
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
//keyProperty和keyColumn使用于<insert>语句
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? new Jdbc3KeyGenerator() : new NoKeyGenerator();
}
//通过解析出来的属性值,构造MappedStatement对象。并放入configuration对象中
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver);
}


<insert>节点的子节点<selectKey>的解析

<selectKey>配置例子:

<selectKey
databaseId="mysql“
keyProperty="id"
resultType="int"
order="BEFORE"
statementType="PREPARED">
select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
</selectKey>


keyProperty:selectKey 语句结果应该被设置的目标属性。就是给哪个字段生成主键值。

resultType:结果的类型。MyBatis 通常可以算出来,但是写上也没有问题。 MyBatis 允许任何简单类型用作主键的类型,包括字符串

statementType:MyBatis 支持 STA TEMENT ,PREPARED 和 CALLABLE 语句的映射类型,分别代表 PreparedStatement 和 CallableStatement 类型。

order:这可以被设置为 BEFORE 或 AFTER。如果设置为 BEFORE,那 么它会首先选择主键, 设置 keyProperty 然后执行插入语句。 如果 设置为 AFTER,那么先执行插入语句,然后是 selectKey 元素- 这和如 Oracle 数据库相似,可以在插入语句中嵌入序列调用.

java代码:

public void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
//可以有多个<selectKey>节点。
for (XNode nodeToHandle : list) {
String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
String databaseId = nodeToHandle.getStringAttribute("databaseId");
if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
}
}
}
//
public void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
String resultType = nodeToHandle.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));

//defaults
boolean useCache = false;
boolean resultOrdered = false;
KeyGenerator keyGenerator = new NoKeyGenerator();
Integer fetchSize = null;
Integer timeout = null;
boolean flushCache = false;
String parameterMap = null;
String resultMap = null;
ResultSetType resultSetTypeEnum = null;
//解析生成主键的sql语句
SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
SqlCommandType sqlCommandType = SqlCommandType.SELECT;
//生成MappedStatement 对象,放入configuration对象的mappedStatements map中
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, null, databaseId, langDriver);

id = builderAssistant.applyCurrentNamespace(id, false);

MappedStatement keyStatement = configuration.getMappedStatement(id, false);
//同时将这个MappedStatement 对象放入到configuration的 keyGenerator map中
configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
nodeToHandle.getParent().getNode().removeChild(nodeToHandle.getNode());
}


下篇会主要看看<resultMap>的解析和sqlSource的生成。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: