阅读完需:约 27 分钟
Mybatis成员
Mybatis的运行分为两大部分:
- 一是
SqlSessionFactory
的创建过程,它主要是通过XMLConfigBuilder
将我们的配置文件读取并且缓存到Configuration
对象中,然后通过Configuration
来创建SqlSessionFactory
对象。 - 二是SqlSession的执行过程,这个过程是Mybatis中最复杂的,它包含了许多复杂的技术,包括反射技术和动态代理技术等,这是Mybatis底层架构的基础。
MyBatis的主要成员组件(成员):
-
SqlSessionFactoryBuilder
:会根据XML配置或是Java配置来生成SqlSessionFactory对象。采用建造者模式(简单来说就是分步构建一个大的对象,例如建造一个大房子,采用购买砖头、砌砖、粉刷墙面的步骤建造,其中的大房子就是大对象,一系列的建造步骤就是分步构建)。 -
SqlSessionFactory
:用于生成SqlSession,可以通过 SqlSessionFactory.openSession() 方法创建 SqlSession 对象。使用工厂模式(简单来说就是我们获取对象是通过一个类,由这个类去创建我们所需的实例并返回,而不是我们自己通过new去创建)。 -
Configuration
:MyBatis所有的配置信息都保存在Configuration对象之中,配置文件中的大部分配置都会存储到该类中。 -
SqlSession
:相当于JDBC中的 Connection对象,可以用 SqlSession 实例来直接执行被映射的 SQL 语句,也可以获取对应的Mapper。 -
Executor
:MyBatis 中所有的 Mapper 语句的执行都是通过 Executor 执行的,负责SQL语句的生成和查询缓存的维护 。 -
StatementHandler
:封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数等。 -
ParameterHandler
:负责对用户传递的参数转换成JDBC Statement 所对应的数据类型。 -
ResultSetHandler
:负责将JDBC返回的ResultSet结果集对象转换成List类型的集合。 -
TypeHandler
:负责java数据类型和jdbc数据类型(也可以说是数据表列类型)之间的映射和转换。 -
MappedStatement
:作用是保存一个映射器节点<select|update|delete|insert>中的内容,主要用途是描述一条SQL语句。MappedStatement封装了Statement的相关信息,包括我们配置的SQL、SQL的id、缓存信息、resultMap、ParameterType、resultType、resultMap等重要配置内容等。Mybatis可以通过它来获取某条SQL配置的所有信息。它还有一个非常重要的属性是SqlSource。 -
SqlSource
:负责提供BoundSql对象的地方。作用就是根据上下文和参数解析生成真正的SQL,然后将信息封装到BoundSql对象中,并返回。我们在Mapper映射文件中定义的SQL,这个SQL可以有占位符和一系列参数的(如select * from t_user where id = #{id}),也可以是动态SQL的形式,这里的SqlSource就是用来将它解析为真正的SQL(如:select * from t_user where id = ?)。注意:SqlSource是一个接口,而不是一个实现类。对它而言有这么几个重要的实现类:DynamicSQLSource
、ProviderSQLSource
、RawSQLSource
、StaticSQLSource
。例如前面动态SQL就采用了DynamicSQLSource
配合参数解析解析后得到的。它算是起到生成真正SQL语句的一个中转站吧。 -
BoundSql
:它是一个结果对象,它是通过SqlSource来获取的。作用是通过SqlSource对映射文件的SQL和参数联合解析得到的真正SQL和参数。什么意思呢?就是BoundSql包含了真正的SQL语句(由SqlSource生成的,如select * from t_user where id = ?),而且还包含了SQL语句增删改查的参数,而SqlSource是负责将映射文件中定义的SQL生成真正的SQL语句(算是映射文件中的SQL生成真正的SQL语句的中转站)。BoundSql有3个常用的属性:sql、parameterObject、parameterMappings,这里就不做讨论了,通过名字应该很容易理解它的用处。
SqlSessionFactory的构建过程
SqlSessionFactory 是MyBatis的核心类之一, 其最重要的功能就是提供创建MyBatis的核心接口SqlSession,所以我们要先创建SqlSessionFactory
,它是通过Builder(建造者)模式来创建的,所以在Mybatis中提供了SqlSessionFactoryBuilder
类。其构建分为两步。
- 第 1 步: 通过 org.apache.ibatis.builder.xml.
XMLConfigBuilder
解析配置的XML文件,读出所配置的参数,并将读取的内容存入org.apache.ibatis.session.Configuration类对象中。而Configuration采用的是单例模式,几乎所有的 MyBatis 配置内容都会存放在这个单例对象中,以便后续将这些内容读出。 - 第2步:使用Confinguration对象去创建SqlSessionFactory。MyBatis 中的 SqlSessionFactory 是一个接口,而不是一个实现类,为此MyBatis提供了一个默认的实现类org.apache.ibatis.session.defaults.
DefaultSqlSessionFactory
。在大部分情况下都没有必要自己去创建新的SqlSessionFactory 实现类,而是由系统创建。
这种创建的方式就是一种 Builder 模式,对于复杂的对象而言,使用构造参数很难实现。这时使用一个类(比如 Configuration)作为统领,一步步地构建所需的内容,然后通过它去创建最终的对象(比如 SqlSessionFactory),这样每一步都会很清晰,这种方式值得大家学习,并且在工作中使用。
下面我们就来学习一下SqlSessionFactory
是如何构建的。程序入口代码如下:
//1、加载 mybatis 全局配置文件
InputStream is = MybatisTest.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
//InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
//2、创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
//3、根据 sqlSessionFactory 来创建sqlSession 对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//4、创建Mapper接口的的代理对象,getMapper方法底层会通过动态代理生成UserMapper的代理实现类
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
①、首先会执行SqlSessionFactoryBuilder
类中的build(InputStream inputStream)方法。
//最初调用SqlSessionFactoryBuilder类中的build
public SqlSessionFactory build(InputStream inputStream) {
//然后调用了重载方法
return build(inputStream, null, null);
}
②、上面的方法中调用了另一个重载的build方法。
//调用的重载方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
//XMLConfigBuilder是专门解析mybatis的配置文件的类
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
//又调用了一个重载方法。parser.parse()的返回值是Configuration对象,这是解析配置文件最核心的方法,非常重要!
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
可以发现其内部定义了一个XMLConfigBuilder
对象,然后通过这个对象调用自身的parse()方法对配置文件进行解析,这个parse()方法的返回值为Configuration
对象,最后将返回的Configuration
对象作为参数调用build()方法,从而完成SqlSessionFactory
的创建。所以我们这里需要注意的就是这两行代码:XMLConfigBuilder对象和调用build(parser.parse())方法返回SqlSessionFactory。
(1)、XMLConfigBuilder
从类名就可以看出,这是用来解析XML配置文件的类,其父类为BaseBuilder。我们来看一下这个类构造方法:
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
/**
* 调用父类的BaseBuilder的构造方法:给
* configuration赋值
* typeAliasRegistry别名注册器赋值
* TypeHandlerRegistry赋值
*/
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
/**
* 把props绑定到configuration的props属性上
*/
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
通过查看XMLConfigBuilder
中构造方法的源码,可以得知XML配置文件最终是由org.apache.ibatis.parsing.XPathParser
封装的XPath
解析的。第一个构造方法通过XPathParser构造方法传入我们读取的XML流文件、Properites流文件和environment等参数得到了一个XpathParser实例对象parser,这里parser已包含全局XML配置文件解析后的所有信息,然后再将parser作为参数传给XMLConfigBuilder
构造方法。其中XMLConfigBuilder
构造方法还调用了父类BaseBuilder的构造方法BaseBuilder
(Configuration),这里传入了一个Configuration
对象,用来初始化Configuration
对象,我们来继续进入看一下:
注意:这里的重点是创建了一个Configuration
对象,并且完成了初始化,这个Configuration是用来封装所有配置文件的类,所以非常非常重要!!!同时还初始化了别名和类型处理器,所以我们默认可以使用这些特性。额外这个父类BaseBuilder
还包含了MapperBuilderAssistant
, SqlSourceBuilder
, XMLConfigBuilder
, XMLMapperBuilder
, XMLScriptBuilder
, XMLStatementBuilder
等子类,这些子类都是用来解析MyBatis各个配置文件,他们通过BaseBuilder父类共同维护一个全局的Configuration对象。只是XMLConfigBuilder
的作用就是解析全局配置文件,调用BaseBuilder其他子类解析其他配置文件,生成最终的Configuration对象。
(2)、然后我们重点来看一下parse()方法,这是最核心的方法。进入parse.parse()方法:
public Configuration parse() {
/**
* 若已经解析过了 就抛出异常
*/
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
/**
* 设置解析标志位
*/
parsed = true;
/**
* 解析我们的mybatis-config.xml的
* 节点
* <configuration>
*
*
* </configuration>
*/
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
注意:XMLConfigBuilder
的 parsed 属性,默认值是false(上面的构造方法中可以看到),它是用来标识XMLConfigBuilder
对象的。当创建了一个XMLConfigBuilder
对象,并进行解析配置文件的时候,parsed的值就变成了true。如果第二次进行解析的时候就会抛出BuilderException
异常,提示每个XMLConfigBuilder
只能使用一次,从而确保了Configuration
对象是单例的。因为Configuration对象是通过XMLConfigBuilder的parse()去解析的。
Configuration对象的具体解析是通过parseConfiguration(XNode root)方法来完成的。这个方法用于解析MyBatis 全局配置文件与SQL 映射文件中的相关配置,参数中”/configuration” 就是对应全局配置文件中的<configuration> 标签,parser 是XPathParser 类的实例(前面已经介绍过了),通过该对象解析XML 配置文件然后把它们解析并保存在Configuration单例中。所以下面我们来看一下这个方法的具体源码:
// 方法实现说明:解析我们mybatis-config.xml的 configuration节点
private void parseConfiguration(XNode root) {
try {
/**
* 解析 properties节点
* <properties resource="mybatis/db.properties" />
* 解析到org.apache.ibatis.parsing.XPathParser#variables
* org.apache.ibatis.session.Configuration#variables
*/
propertiesElement(root.evalNode("properties"));
/**
* 解析我们的mybatis-config.xml中的settings节点
* 具体可以配置哪些属性:http://www.mybatis.org/mybatis-3/zh/configuration.html#settings
* <settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="mapUnderscoreToCamelCase" value="false"/>
<setting name="localCacheScope" value="SESSION"/>
<setting name="jdbcTypeForNull" value="OTHER"/>
..............
</settings>
*
*/
Properties settings = settingsAsProperties(root.evalNode("settings"));
/**
* 基本没有用过该属性
* VFS含义是虚拟文件系统;主要是通过程序能够方便读取本地文件系统、FTP文件系统等系统中的文件资源。
Mybatis中提供了VFS这个配置,主要是通过该配置可以加载自定义的虚拟文件系统应用程序
解析到:org.apache.ibatis.session.Configuration#vfsImpl
*/
loadCustomVfs(settings);
/**
* 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。
* SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING
* 解析到org.apache.ibatis.session.Configuration#logImpl
*/
loadCustomLogImpl(settings);
/**
* 解析我们的别名
* <typeAliases>
<typeAlias alias="Author" type="cn.tulingxueyuan.pojo.Author"/>
</typeAliases>
<typeAliases>
<package name="cn.tulingxueyuan.pojo"/>
</typeAliases>
解析到oorg.apache.ibatis.session.Configuration#typeAliasRegistry.typeAliases
*/
typeAliasesElement(root.evalNode("typeAliases"));
/**
* 解析我们的插件(比如分页插件)
* mybatis自带的
* Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
解析到:org.apache.ibatis.session.Configuration#interceptorChain.interceptors
*/
pluginElement(root.evalNode("plugins"));
/**
* todo
*/
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 设置settings 和默认值
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
/**
* 解析我们的mybatis环境
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="root"/>
<property name="password" value="Zw726515"/>
</dataSource>
</environment>
<environment id="test">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
* 解析到:org.apache.ibatis.session.Configuration#environment
* 在集成spring情况下由 spring-mybatis提供数据源 和事务工厂
*/
environmentsElement(root.evalNode("environments"));
/**
* 解析数据库厂商
* <databaseIdProvider type="DB_VENDOR">
<property name="SQL Server" value="sqlserver"/>
<property name="DB2" value="db2"/>
<property name="Oracle" value="oracle" />
<property name="MySql" value="mysql" />
</databaseIdProvider>
* 解析到:org.apache.ibatis.session.Configuration#databaseId
*/
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
/**
* 解析我们的类型处理器节点
* <typeHandlers>
<typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>
解析到:org.apache.ibatis.session.Configuration#typeHandlerRegistry.typeHandlerMap
*/
typeHandlerElement(root.evalNode("typeHandlers"));
/**
* 最最最最最重要的就是解析我们的mapper
*
resource:来注册我们的class类路径下的
url:来指定我们磁盘下的或者网络资源的
class:
若注册Mapper不带xml文件的,这里可以直接注册
若注册的Mapper带xml文件的,需要把xml文件和mapper文件同名 同路径
-->
<mappers>
<mapper resource="mybatis/mapper/EmployeeMapper.xml"/>
<mapper class="com.tuling.mapper.DeptMapper"></mapper>
<package name="com.tuling.mapper"></package>
-->
</mappers>
* package 1.解析mapper接口 解析到:org.apache.ibatis.session.Configuration#mapperRegistry.knownMappers
2.
*/
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
以上操作会把MyBatis 全局配置文件与SQL 映射文件中每一个节点的信息都读取出来,然后保存在Configuration单例中,Configuration分别对以下内容做出了初始化:properties
属性 ;typeAliases
类型别名;plugins
插件;objectFactory
对象工厂;settings
设置;environments
环境;databaseIdProvider
数据库厂商标识;typeHandlers
类型处理器;mappers
映射器等。这其中还涉及到了很多的方法,在这里就不一一讲述了,大家可以自己进行查看。这里主要来看一下mappers
映射器,因为我们需要频繁的访问它,因此它算是这里最重要的内容了吧。
进入mapperElement(XNode parent):
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
/**
* 获取我们mappers节点下的一个一个的mapper节点
*/
for (XNode child : parent.getChildren()) {
/**
* 判断我们mapper是不是通过批量注册的
* <package name="com.tuling.mapper"></package>
*/
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
/**
* 判断从classpath下读取我们的mapper
* <mapper resource="mybatis/mapper/EmployeeMapper.xml"/>
*/
String resource = child.getStringAttribute("resource");
/**
* 判断是不是从我们的网络资源读取(或者本地磁盘得)
* <mapper url="D:/mapper/EmployeeMapper.xml"/>
*/
String url = child.getStringAttribute("url");
/**
* 解析这种类型(要求接口和xml在同一个包下)
* <mapper class="com.tuling.mapper.DeptMapper"></mapper>
*
*/
String mapperClass = child.getStringAttribute("class");
/**
* 我们得mappers节点只配置了
* <mapper resource="mybatis/mapper/EmployeeMapper.xml"/>
*/
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
/**
* 把我们的文件读取出一个流
*/
InputStream inputStream = Resources.getResourceAsStream(resource);
/**
* 创建读取XmlMapper构建器对象,用于来解析我们的mapper.xml文件
*/
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
/**
* 真正的解析我们的mapper.xml配置文件(说白了就是来解析我们的sql)
*/
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
上面的代码遍历mappers标签下所有子节点,其中:
- 如果遍历到package子节点,是以包名引入映射器,则将该包下所有Class注册到
Configuration
的mapperRegistry
中。 - 如果遍历到mapper子节点的class属性,是以class的方式引入映射器,则将制定的Class注册到注册到
Configuration
的mapperRegistry
中。 - 如果遍历到mapper子节点的resource或者url属性,是通过resource或者url方式引入映射器,则直接对资源文件进行解析:
所以在通过resource或者url方式引入映射器的代码中,可以注意到定义了一个XMLMapperBuilder
类,然后调用了parse()方法,这目的就很明显了,如果遍历到是以mapper子节点的resource或者url属性方式引入的映射器,那么所有的Sql映射文件都是用XMLMapperBuilder
类的parse()方法来进行解析。
注意:其实到这里就已经可以不用往下看了,如果你想更加深入了解一下,那就继续往下滑吧!!!
我们先分别来看一下resource或者url属性方式引入映射器的构造方法(它两共用一个):
XPathParser
将mapper配置文件解析成Document
对象后封装到一个XPathParser
对象,再将XPathParser
对象作为参数传给XMLMapperBuilder
构造方法并构造出一个XMLMapperBuilder
对象,XMLMapperBuilder对象的builderAssistant字段是一个MapperBuilderAssistant对象,同样也是BaseBuilder的一个子类,其作用是对MappedStatement
对象进行封装。
有了XMLMapperBuilder对象后,就可以进入解析mapper映射文件的过程,进入parse()方法:
/**
* 方法实现说明:真正的去解析我们的Mapper.xml(EmployeeMapper.xml)
* @author:xsls
* @return:
* @exception:
* @date:2019/8/30 16:43
*/
public void parse() {
/**
* 判断当前的Mapper是否被加载过
*/
if (!configuration.isResourceLoaded(resource)) {
/**
* 真正的解析我们的 <mapper namespace="com.tuling.mapper.EmployeeMapper">
*
*/
configurationElement(parser.evalNode("/mapper"));
/**
* 把资源保存到我们Configuration中
*/
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
调用XMLMapperBuilder的configurationElement方法,对mapper映射文件进行解析
/**
* 方法实现说明:解析我们的<mapper></mapper>节点
* @author:xsls
* @param context document节点
* @return:
* @exception:
* @date:2019/8/31 13:34
*/
private void configurationElement(XNode context) {
try {
/**
* 解析我们的namespace属性
* <mapper namespace="com.tuling.mapper.EmployeeMapper">
*/
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
/**
* 保存我们当前的namespace 并且判断接口完全类名==namespace
*/
builderAssistant.setCurrentNamespace(namespace);
/**
* 解析我们的缓存引用
* 说明我当前的缓存引用和DeptMapper的缓存引用一致
* <cache-ref namespace="com.tuling.mapper.DeptMapper"></cache-ref>
解析到org.apache.ibatis.session.Configuration#cacheRefMap<当前namespace,ref-namespace>
异常下(引用缓存未使用缓存):org.apache.ibatis.session.Configuration#incompleteCacheRefs
*/
cacheRefElement(context.evalNode("cache-ref"));
/**
* 解析我们的cache节点
* <cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache>
解析到:org.apache.ibatis.session.Configuration#caches
org.apache.ibatis.builder.MapperBuilderAssistant#currentCache
*/
cacheElement(context.evalNode("cache"));
/**
* 解析paramterMap节点(该节点mybaits3.5貌似不推荐使用了)
*/
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
/**
* 解析我们的resultMap节点
* 解析到:org.apache.ibatis.session.Configuration#resultMaps
* 异常 org.apache.ibatis.session.Configuration#incompleteResultMaps
*
*/
resultMapElements(context.evalNodes("/mapper/resultMap"));
/**
* 解析我们通过sql节点
* 解析到org.apache.ibatis.builder.xml.XMLMapperBuilder#sqlFragments
* 其实等于 org.apache.ibatis.session.Configuration#sqlFragments
* 因为他们是同一引用,在构建XMLMapperBuilder 时把Configuration.getSqlFragments传进去了
*/
sqlElement(context.evalNodes("/mapper/sql"));
/**
* 解析我们的select | insert |update |delete节点
* 解析到org.apache.ibatis.session.Configuration#mappedStatements
*/
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);
}
}
mapper映射文件必须有namespace
属性值,否则抛出异常,将namespace
属性保存到XMLMapperBuilder
的MapperBuilderAssistant
对象中,以便其他方法调用。
该方法对mapper映射文件每个标签逐一解析并保存到Configuration
和MapperBuilderAssistant
对象中,最后调用buildStatementFromContext
方法解析select、insert、update和delete
节点。
private void buildStatementFromContext(List<XNode> list) {
/**
* 判断有没有配置数据库厂商ID
*/
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
/**
* 方法实现说明:解析我们得得select|update|delte|insert节点然后
* 创建我们得mapperStatment对象
* @author:xsls
* @param list:所有的select|update|delte|insert节点
* @param requiredDatabaseId:判断有没有数据库厂商Id
* @return:
* @exception:
* @date:2019/9/5 21:35
*/
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
/**
* 循环我们的select|delte|insert|update节点
*/
for (XNode context : list) {
/**
* 创建一个xmlStatement的构建器对象
*/
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
buildStatementFromContext
方法中调用XMLStatementBuilder
的parseStatementNode()
方法来完成解析。
public void parseStatementNode() {
/**
* 我们的insert|delte|update|select 语句的sqlId
*/
String id = context.getStringAttribute("id");
/**
* 判断我们的insert|delte|update|select 节点是否配置了
* 数据库厂商标注
*/
String databaseId = context.getStringAttribute("databaseId");
/**
* 匹配当前的数据库厂商id是否匹配当前数据源的厂商id
*/
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
/**
* 获得节点名称:select|insert|update|delete
*/
String nodeName = context.getNode().getNodeName();
/**
* 根据nodeName 获得 SqlCommandType枚举
*/
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
/**
* 判断是不是select语句节点
*/
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
/**
* 获取flushCache属性
* 默认值为isSelect的反值:查询:默认flushCache=false 增删改:默认flushCache=true
*/
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
/**
* 获取useCache属性
* 默认值为isSelect:查询:默认useCache=true 增删改:默认useCache=false
*/
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
/**
* resultOrdered: 是否需要处理嵌套查询结果 group by (使用极少)
* 可以将比如 30条数据的三组数据 组成一个嵌套的查询结果
*/
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
/**
* 解析我们的sql公用片段
* <select id="qryEmployeeById" resultType="Employee" parameterType="int">
<include refid="selectInfo"></include>
employee where id=#{id}
</select>
将 <include refid="selectInfo"></include> 解析成sql语句 放在<select>Node的子节点中
*/
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
/**
* 解析我们sql节点的参数类型
*/
String parameterType = context.getStringAttribute("parameterType");
// 把参数类型字符串转化为class
Class<?> parameterTypeClass = resolveClass(parameterType);
/**
* 查看sql是否支撑自定义语言
* <delete id="delEmployeeById" parameterType="int" lang="tulingLang">
<settings>
<setting name="defaultScriptingLanguage" value="tulingLang"/>
</settings>
*/
String lang = context.getStringAttribute("lang");
/**
* 获取自定义sql脚本语言驱动 默认:class org.apache.ibatis.scripting.xmltags.XMLLanguageDriver
*/
LanguageDriver langDriver = getLanguageDriver(lang);
// Parse selectKey after includes and remove them.
/**
* 解析我们<insert 语句的的selectKey节点, 还记得吧,一般在oracle里面设置自增id
*/
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
/**
* 我们insert语句 用于主键生成组件
*/
KeyGenerator keyGenerator;
/**
* selectById!selectKey
* id+!selectKey
*/
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
/**
* 把我们的命名空间拼接到keyStatementId中
* com.tuling.mapper.Employee.saveEmployee!selectKey
*/
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
/**
*<insert id="saveEmployee" parameterType="com.tuling.entity.Employee" useGeneratedKeys="true" keyProperty="id">
*判断我们全局的配置类configuration中是否包含以及解析过的组件生成器对象
*/
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
/**
* 若我们配置了useGeneratedKeys 那么就去除useGeneratedKeys的配置值,
* 否者就看我们的mybatis-config.xml配置文件中是配置了
* <setting name="useGeneratedKeys" value="true"></setting> 默认是false
* 并且判断sql操作类型是否为insert
* 若是的话,那么使用的生成策略就是Jdbc3KeyGenerator.INSTANCE
* 否则就是NoKeyGenerator.INSTANCE
*/
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
/**
* 通过class org.apache.ibatis.scripting.xmltags.XMLLanguageDriver来解析我们的
* sql脚本对象 . 解析SqlNode. 注意, 只是解析成一个个的SqlNode, 并不会完全解析sql,因为这个时候参数都没确定,动态sql无法解析
*/
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
/**
* STATEMENT,PREPARED 或 CALLABLE 中的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED
*/
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
/**
* 这是一个给驱动的提示,尝试让驱动程序每次批量返回的结果行数和这个设置值相等。 默认值为未设置(unset)(依赖驱动)
*/
Integer fetchSize = context.getIntAttribute("fetchSize");
/**
* 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖驱动)。
*/
Integer timeout = context.getIntAttribute("timeout");
/**
* 将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler) 推断出具体传入语句的参数,默认值为未设置
*/
String parameterMap = context.getStringAttribute("parameterMap");
/**
* 从这条语句中返回的期望类型的类的完全限定名或别名。 注意如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身。
* 可以使用 resultType 或 resultMap,但不能同时使用
*/
String resultType = context.getStringAttribute("resultType");
/**解析我们查询结果集返回的类型 */
Class<?> resultTypeClass = resolveClass(resultType);
/**
* 外部 resultMap 的命名引用。结果集的映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂映射的情形都能迎刃而解。
* 可以使用 resultMap 或 resultType,但不能同时使用。
*/
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
/**
* 解析 keyProperty keyColumn 仅适用于 insert 和 update
*/
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
/**
* 为我们的insert|delete|update|select节点构建成我们的mappedStatment对象
*/
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
注意:解析所有的Sql语句会封装一个MappedStatement
中,MappedStatement
中包含了许多我们配置的SQL、SQL的id、缓存信息、resultMap、ParameterType、resultType、resultMap等重要配置内容。最重要的是它还有一个属性sqlSource
。
通过上面的方法可以看到SQL语句封装到一个SqlSource
对象,SqlSource
是个接口,如果是动态SQL就创建DynamicSqlSource
实现类,否则创建StaticSqlSource
实现类。
SqlSource
是MappedStatement
的一个属性,它只是一个接口。它的主要作用是根据上下文和参数解析生成需要的Sql。
SqlSource接口中有一个getBoundSql
方法,这个方法就是用来获取BoundSql的:
SqlSource接口中还有如下这几个重要的实现类:
BoundSql是一个结果集对象,也就是SqlSource通过对映射文件的SQL和参数解析得到的真正的SQL和参数。
注:MappedStatement
、SqlSource
和BoundSql
在最上面已经详细的介绍了,自行滑到上面查看。
③、Mybatis的配置文件解析完成后,会将信息保存在Configuration
对象中,之后通过XMLConfigBuilder
类中的parse()方法返回,然后再将Configuration对象作为参数传递到build(Configuraction config)方法。进入这个build方法:
// SqlSessionFactoryBuilder另一个build方法
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
可以看到最后接着返回一个DefaultSqlSessionFactory
对象,DefaultSqlSessionFactory
就是SqlSessionFactory
的一个实现类,到这里SqlSessionFactory
对象就完成了创建的全部过程。
SqlSessionFactory构建过程中的时序图: