这篇文章是自己工作中记录下来的,由于工作比较忙,一直留存于本地忘了传到自己的博客中,现在优化了一下文章结构传上来,请各位看官批评指正。
另外,后续会有很多自己工作中记录的知识点文章做二次优化传上来。
在开发大数据平台调度系统过程中,我们通过spring boot快速构建调度平台,持久化框架采用Mybatis。这也是初次使用Mybatis,当然工作中只是构建一些Dao、Mapper,使用一些增删改查完成调度任务的历史记录、任务间的Dependency的记录。但是Mybatis的基本运行原理还是需要结合源码来梳理一下的。
这里我举一个简单的Demo,跟着Demo一步一步的去看源码,认识到Mybatis的运行原理。
第一部分:Demo样例
创建User实体类:
@Data
public class MyUser {
private Integer uid;
private String uname;
private String usex;
}
创建UserDao接口:
import java.util.List;
import gzc.entity.MyUser;
public interface UserDao {
// 接口定义的方法名与Mapper映射id一致
public MyUser selectUserById(Integer uid);
public List<MyUser> selectAllUser();
public int addUser(MyUser user);
public int updateUser(MyUser user);
public int deleteUser(Integer uid);
}
创建映射文件:MyUserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace属性绑定dao接口 -->
<mapper namespace="gzc.dao.UserDao">
<!-- 根据uid 查询一个用户信息 -->
<select id="selectUserById" resultType="zjw.entity.MyUser" parameterType="Integer">
select * from myuser where uid = #{uid}
</select>
<!-- 查询全部用户信息 -->
<select id="selectAllUser" resultType="zjw.entity.MyUser">
select * from myuser
</select>
<!-- 添加一个用户 #{uname} 为gzc.entity.MyUser的属性值-->
<insert id="addUser" parameterType="zjw.entity.MyUser">
insert into myuser values(#{uid},#{uname},#{usex})
</insert>
<!-- 修改一个用户 -->
<update id="updateUser" parameterType="zjw.entity.MyUser">
update myuser set uname=#{uname},usex=#{usex} where uid=#{uid}
</update>
<!-- 删除一个用户 -->
<delete id="deleteUser" parameterType="zjw.entity.MyUser">
delete from myuser where uid=#{uid}
</delete>
</mapper>
创建Mybatis主配置文件mybatis-config.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--利用mybatis自带环境配置数据源,以后和Spring整合后,这部分数据源可以交由Spring 配置处理-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/springtestdb?serverTimezone=UTC" />
<property name="username" value="root" />
<property name="password" value="123456" />
</dataSource>
</environment>
</environments>
<mappers>
<!-- SQL映射文件的位置-->
<mapper resource="zjw/mapper/MyUserMapper.xml" />
</mappers>
</configuration>
创建测试类:
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import zjw.dao.UserDao;
import zjw.entity.MyUser;
public class MybatisTest {
public static void main(String[] args) {
try {
// 读取配置文件mybatis-config.xml
InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
// 根据配置文件构建SqlSessionFactory
SqlSessionFactory ssl = new SqlSessionFactoryBuilder().build(config);
// 通过SqlSessionFactory创建SQLSession对象
SqlSession ss = ssl.openSession();
/*
* 方法一 : SqlSession执行映射文件中定义的sql,并返回映射结果
* gzc.mapper.MyUserMapper.selectUserById为MyUserMapper.xml中的命名空间+SQL语句的id 例如:
* MyUser mu = ss.selectOne("gzc.mapper.MyUserMapper.selectUserById", 6);
*/
/*
* 方法二 : 通过SqlSession对象getMapper方法获得Mapper映射与Dao接口映射
* 该方法需要绑定dao的接口到Mapper的namespace中
*/
// 将dao接口方法与映射文件关联,返回接口对象
UserDao userDao = ss.getMapper(UserDao.class);
// 查询一个用户
MyUser user = userDao.selectUserById(1);
System.out.println(user);
// 添加一个用户
MyUser newUser = new MyUser(8, "小花", "女");
userDao.addUser(newUser);
// 修改一个用户
MyUser updatemu = new MyUser(7, "小明", "男");
userDao.updateUser(updatemu);
// 删除一个用户
userDao.deleteUser(3);
// 查找所有用户
List<MyUser> myUsers = userDao.selectAllUser();
for (MyUser myUser : myUsers) {
System.out.println(myUser);
}
ss.commit();
ss.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
Mybatis重要组件和运行流程图
- Configuration:Mybatis所有的配置信息都保存在Configuration对象中,配置文件中的大部分配置都会存储到该类
- SqlSession:作为Mybatis工作的顶层API,表示和数据库交互时的会话,完成必要数据库增删改查功能
- Executor:Mybatis执行器,是Mybatis调度的核心,负责SQL语句的生成和查询缓存的维护
- StatementHandler:封装了JDBC Statement操作,负责对JDBC Statement的操作,如设置参数等
- ParameterHandler:负责对用户传递的参数转换成JDBC Statement所对应的数据类型
- ResultSetHandler:负责将JDBC返回的ResultSet结果集对象转换成List类型的集合
- TypeHandler:负责java数据类型和jdbc数据类型之间的映射和转换
- MappedStatement:MappedStatement维护一条<select|update|delete|insert>节点的封装
- SqlSource:负责根据用户传递的parameterObject,动态生成SQL语句,将信息封装到BoundSql对象中,并返回
- BoundSql:表示动态生成的SQL语句以及相应的参数信息
真正的源码分析来啦
测试类贴过来方便看:
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import zjw.dao.UserDao;
import zjw.entity.MyUser;
public class MybatisTest {
public static void main(String[] args) {
try {
// 读取配置文件mybatis-config.xml
InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
// 根据配置文件构建SqlSessionFactory
SqlSessionFactory ssl = new SqlSessionFactoryBuilder().build(config);
// 通过SqlSessionFactory创建SQLSession对象
SqlSession ss = ssl.openSession();
/*
* 方法一 : SqlSession执行映射文件中定义的sql,并返回映射结果
* gzc.mapper.MyUserMapper.selectUserById为MyUserMapper.xml中的命名空间+SQL语句的id 例如:
* MyUser mu = ss.selectOne("gzc.mapper.MyUserMapper.selectUserById", 6);
*/
/*
* 方法二 : 通过SqlSession对象getMapper方法获得Mapper映射与Dao接口映射
* 该方法需要绑定dao的接口到Mapper的namespace中
*/
// 将dao接口方法与映射文件关联,返回接口对象
UserDao userDao = ss.getMapper(UserDao.class);
// 查询一个用户
MyUser user = userDao.selectUserById(1);
System.out.println(user);
// 添加一个用户
MyUser newUser = new MyUser(8, "小花", "女");
userDao.addUser(newUser);
// 修改一个用户
MyUser updatemu = new MyUser(7, "小明", "男");
userDao.updateUser(updatemu);
// 删除一个用户
userDao.deleteUser(3);
// 查找所有用户
List<MyUser> myUsers = userDao.selectAllUser();
for (MyUser myUser : myUsers) {
System.out.println(myUser);
}
ss.commit();
ss.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
这是Mybatis操作数据库的基本步骤。
InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
资源加载mybatis的主配置文件获取输入流对象。我们重点看下一行代码:
// 根据配置文件构建SqlSessionFactory
SqlSessionFactory ssl = new SqlSessionFactoryBuilder().build(config);
这行代码表示根据主配置文件的流对象构建一个会话工厂对象。这里用到了建造者模式:要创建某个对象不直接new,而是利用其它的类来创建这个对象。mybatis的所有初始化工作都是这行代码完成的,我们跟着源码深入去看。
一:进入build方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
//委托XMLConfigBuilder来解析xml文件,并构建
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
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对象,这个对象的作用就是解析主配置文件用的。我们可以发现主配置文件的最外层节点是标签,mybatis的初始化就是把这个标签及其所有子标签进行解析,把解析好的数据封装在Configuration这个类中。
二:进入parse()方法
//解析配置
public Configuration parse() {
//如果已经解析过了,报错
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//根节点是configuration
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
XMLConfigBuilder维护一个parsed属性默认为false,此方法首先判断主配置文件是否已经被解析,如果解析过了就抛异常。
三:进入parseConfiguration方法
private void parseConfiguration(XNode root) {
try {
//分步骤解析
//issue #117 read properties first
//1.properties
propertiesElement(root.evalNode("properties"));
//2.类型别名
typeAliasesElement(root.evalNode("typeAliases"));
//3.插件
pluginElement(root.evalNode("plugins"));
//4.对象工厂
objectFactoryElement(root.evalNode("objectFactory"));
//5.对象包装工厂
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//6.设置
settingsElement(root.evalNode("settings"));
// read it after objectFactory and objectWrapperFactory issue #631
//7.环境
environmentsElement(root.evalNode("environments"));
//8.databaseIdProvider
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//9.类型处理器
typeHandlerElement(root.evalNode("typeHandlers"));
//10.映射器
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
此方法很明显是对的所有子标签逐个解析。比如常在配置文件中出现的settings属性配置,在settings会配置缓存,日志等。typeAliases是配置别名。environments是配置数据库链接和事务。这些子节点会被一个个解析并把解析后的数据封装在Configuration类中,可以看到第二部方法的返回值就是Configuration对象。我们重点分析mappers标签,这个标签中还有一个个的mapper标签去映射mapper所对应的mapper.xml。
四:进入mapperElement方法
// 10.4自动扫描包下所有映射器
// <mappers>
// <package name="org.mybatis.builder"/>
// </mappers>
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
//10.4自动扫描包下所有映射器
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
//10.1使用类路径
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
//映射器比较复杂,调用XMLMapperBuilder
//注意在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
//10.2使用绝对url路径
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
//映射器比较复杂,调用XMLMapperBuilder
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
//10.3使用java类名
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节点下面可能会有很多mapper节点。在应用中肯定不止一个mapper.xml。所以他会去遍历每一个mappers节点去解析该节点所映射的xml文件。
- 循环下面是一个if else判断。它先判断mappers下面的子节点是不是package节点。因为在实际开发中很多的xml文件,不可能每一个xml文件都用一个mapper节点去映射,我们直接用一个package节点去映射一个包下面的所有的xml,这是多文件映射。
- 如果不是package节点,那肯定就是mapper节点做单文件映射。我们通过下面的代码发现单文件映射有3种方式,第一种使用mapper节点的resource属性直接映射xml文件。第二种是使用mapper节点url属性映射磁盘内的某个xml文件。第三种是使用mapper节点的class属性直接映射某个mapper接口。
我们主要看单文件映射的resource方式。
五:看resource方式解析xml
if (resource != null && url == null && mapperClass == null) {
//10.1使用类路径
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
//映射器比较复杂,调用XMLMapperBuilder
//注意在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
- 第一行代码是说实例化一个错误上下文对象。这个对象的作用就是把错误信息封装起来,如果出现错误就会调用这个对象的toString方法。此方法的resource参数就是String类型的xml名字,对应的就是mapper的xml文件。提示使用者错误发生在哪个xml文件中。
- 第二行表示读取这个xml获取输入流对象。
- 然后创建一个mapper的xml文件解析器。
六:进入parse方法
//解析
public void parse() {
//如果没有加载过再加载,防止重复加载
if (!configuration.isResourceLoaded(resource)) {
//配置mapper
configurationElement(parser.evalNode("/mapper"));
//标记一下,已经加载过了
configuration.addLoadedResource(resource);
//绑定映射器到namespace
bindMapperForNamespace();
}
//还有没解析完的东东这里接着解析?
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}
最开始判断这个xml文件是否被解析过。因为Configuration对象会维护一个String类型的set集合loadedResources,这个集合中存放了所有已经被解析过的xml文件的名字。由于第一次解析,所有直接进入if逻辑判断。
七:进入ConfigurationElement方法
//配置mapper元素
// <mapper namespace="org.mybatis.example.BlogMapper">
// <select id="selectBlog" parameterType="int" resultType="Blog">
// select * from Blog where id = #{id}
// </select>
// </mapper>
private void configurationElement(XNode context) {
try {
//1.配置namespace
String namespace = context.getStringAttribute("namespace");
if (namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
//2.配置cache-ref
cacheRefElement(context.evalNode("cache-ref"));
//3.配置cache
cacheElement(context.evalNode("cache"));
//4.配置parameterMap(已经废弃,老式风格的参数映射)
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//5.配置resultMap(高级功能)
resultMapElements(context.evalNodes("/mapper/resultMap"));
//6.配置sql(定义可重用的 SQL 代码段)
sqlElement(context.evalNodes("/mapper/sql"));
//7.配置select|insert|update|delete TODO
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
此方法就是解析一个mapper.xml所有节点数据。比如解析namespace,resultMap等等。重点是最后一行代码:
//7.配置select|insert|update|delete TODO
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
我们跟进去看一下:
//7.配置select|insert|update|delete
private void buildStatementFromContext(List<XNode> list) {
//调用7.1构建语句
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
继续进入buildStatementFromContext()
//7.1构建语句
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
//构建所有语句,一个mapper下可以有很多select
//语句比较复杂,核心都在这里面,所以调用XMLStatementBuilder
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
//核心XMLStatementBuilder.parseStatementNode
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
//如果出现SQL语句不完整,把它记下来,塞到configuration去
configuration.addIncompleteStatement(statementParser);
}
}
}
一开始是一个循环,遍历一个list,这个list里装的是xml中的所有sql节点,比如select、insert、update、delete。每一个sql是一个节点,循环解析每一个sql节点
八:进入parseStatementNode()方法
public void parseStatementNode() {
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");
//引用外部 parameterMap,已废弃
String parameterMap = context.getStringAttribute("parameterMap");
//参数类型
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
//引用外部的 resultMap(高级功能)
String resultMap = context.getStringAttribute("resultMap");
//结果类型
String resultType = context.getStringAttribute("resultType");
//脚本语言,mybatis3.2的新功能
String lang = context.getStringAttribute("lang");
//得到语言驱动
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType);
//结果集类型,FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE 中的一种
String resultSetType = context.getStringAttribute("resultSetType");
//语句类型, STATEMENT|PREPARED|CALLABLE 的一种
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
//获取命令类型(select|insert|update|delete)
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
//是否要缓存select结果
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
//仅针对嵌套结果 select 语句适用:如果为 true,就是假设包含了嵌套结果集或是分组了,这样的话当返回一个主结果行的时候,就不会发生有对前面结果集的引用的情况。
//这就使得在获取嵌套的结果集的时候不至于导致内存不够用。默认值:false。
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
//解析之前先解析<include>SQL片段
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// Parse selectKey after includes and remove them.
//解析之前先解析<selectKey>
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
//解析成SqlSource,一般是DynamicSqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
//(仅对 insert 有用) 标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值
String keyProperty = context.getStringAttribute("keyProperty");
//(仅对 insert 有用) 标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值
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();
}
//又去调助手类
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
大致意思就是解析这个sql标签里的所有数据,并把所有数据通过addMappedStatement方法封装在MappedStatement对象中。这个对象封装了一条sql所在标签的所有内容,比如这个sql标签的id,sql语句,入参,出参等等。
九:进入addMappedStatement()方法
//增加映射语句
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
//为id加上namespace前缀
id = applyCurrentNamespace(id, false);
//是否是select语句
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//又是建造者模式
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType);
statementBuilder.resource(resource);
statementBuilder.fetchSize(fetchSize);
statementBuilder.statementType(statementType);
statementBuilder.keyGenerator(keyGenerator);
statementBuilder.keyProperty(keyProperty);
statementBuilder.keyColumn(keyColumn);
statementBuilder.databaseId(databaseId);
statementBuilder.lang(lang);
statementBuilder.resultOrdered(resultOrdered);
statementBuilder.resulSets(resultSets);
setStatementTimeout(timeout, statementBuilder);
//1.参数映射
setStatementParameterMap(parameterMap, parameterType, statementBuilder);
//2.结果映射
setStatementResultMap(resultMap, resultType, resultSetType, statementBuilder);
setStatementCache(isSelect, flushCache, useCache, currentCache, statementBuilder);
MappedStatement statement = statementBuilder.build();
//建造好调用configuration.addMappedStatement
configuration.addMappedStatement(statement);
return statement;
}
我们只看最后三行代码:
- MappedStatement statement = statementBuilder.build(); 通过解析出的参数构建一个MapperStatement对象
- configuration.addMappedStatement(statement),这行是把解析出来的MapperStatement装到Configuration维护的Map集合中。key值是这个sql标签的id值,我们这里应该就是selectUserById等,value值就是我们解析出来的MapperStatement对象。
很明显我们解析xml的目的就是把每个xml中的每个增删改查的sql标签解析成一个个MapperStatement并把解析出来的这些对象装到Configuration的Map中备用。
十:我们返回到第六步代码中
public void parse() {
//如果没有加载过再加载,防止重复加载
if (!configuration.isResourceLoaded(resource)) {
//配置mapper
configurationElement(parser.evalNode("/mapper"));
//标记一下,已经加载过了
configuration.addLoadedResource(resource);
//绑定映射器到namespace
bindMapperForNamespace();
}
//还有没解析完的东东这里接着解析?
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}
从第六步开始一直到第九步都是在执行ConfigurationElement(parser.evalNode("/mapper"))这行代码。接下来看下一行代码configuration.addLoadedResource(resource);在第九步的时候我们已经把一个xml完全解析完毕,所以在此就会把这个解析完的xml名字装到set集合中。
下面我们进入bindMapperForNamespace()方法,通过命名空间绑定mapper。
第十一:进入bindMapperForNamespace()方法
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
if (!configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType);
}
}
}
}
- 首先获取名称空间,名称空间一般都是mapper的全限定名,通过反射获取这个mapper的class对象。
- if判断中,Configuration中也维护了一个Map对象,key值是我们刚才通过反射生产的mapper的class对象,value值是通过动态代理生产的class对象的代理对象。
- 因为Map中还没有添加生产的mapper对象,所以进入到if中,先把名称空间存到刚才保存xml名字的set集合中,然后再把生产的mapper的class对象存到Mapper中。
第十二步:进入addMapper方法
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
调用了mapperRegistry的addMapper方法,这个类是mapper注册类
//看一下如何添加一个映射
public <T> void addMapper(Class<T> type) {
//mapper必须是接口!才会添加
if (type.isInterface()) {
if (hasMapper(type)) {
//如果重复添加了,报错
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<T>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
//如果加载过程中出现异常需要再将这个mapper从mybatis中删除,这种方式比较丑陋吧,难道是不得已而为之?
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
mapperRegistry维护的Map的名字是knownMappers。可以看到put的key值是生成的mapper的class对象,value是通过动态代理生成的mapper的代理对象。
到此为止mybatis根据主配置文件初始化就完成了。总结来说就是:
- 解析主配置文件,将主配置文件中的所有信息封装到Configuration对象中。
- 详细来说就是通过XmlConfigBuilder解析主配置文件,通过XmlMapperBuild解析mappers下映射的所有xml文件(循环解析)。把每个xml中的各个sql解析成一个个MapperStatement对象,装在Configuration维护的一个Map集合中,key是id,value是MapperStatement对象。然后把解析过的xml的名字和名称空间装在set集合中,通过名称空间反射生成的mapper的class对象以及class对象的代理对象装在Configuration对象维护的mapperRegistry中的Map中。
- 我们用resource引入xml的方法是先解析xml ,把各个sql标签解析成mapperstatement对象装进集合,然后再把mapper接口的class对象以及代理对象装进集合,但是引入xml的方式有4种,其中单文件引入方式还有url方式和class方式,看源码可以知道url方式就是直接引入一个xml和resource方式一模一样。而class方式是引入一个mapper接口却同(resource和url方式相反)
十三:多文件映射
if ("package".equals(child.getName())) {
//10.4自动扫描包下所有映射器
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
}
它首先获得xml所在的包名,然后调用Configuration的addMappers对象。多文件映射是addMappers
//将包下所有类加入到mapper
public void addMappers(String packageName, Class<?> superType) {
mapperRegistry.addMappers(packageName, superType);
}
public void addMappers(String packageName, Class<?> superType) {
//查找包下所有是superType的类
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
通过ResolverUtil解析工具找出该package下的所有mapper的名称,通过反射生产mapper的class对象装进集合中,然后循环调用addMapper(mapperClass)方法。这样就和单文件映射的class类型一样,把mapper接口的class对象作为参数传进去,然后生产代理对象装进集合然后再解析xml。
获取session会话对象源码分析
// 根据配置文件构建SqlSessionFactory
SqlSessionFactory ssl = new SqlSessionFactoryBuilder().build(config);
// 通过SqlSessionFactory创建SQLSession对象
SqlSession ss = ssl.openSession();
直接open一个session。session是我们与数据库互动的顶级API,所有的增删改查都要调用session,我们进入opensession方法
/最终都会调用2种方法:openSessionFromDataSource,openSessionFromConnection
//以下6个方法都会调用openSessionFromDataSource
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
//通过事务工厂来产生一个事务
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//生成一个执行器(事务包含在执行器里)
final Executor executor = configuration.newExecutor(tx, execType);
//然后产生一个DefaultSqlSession
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
//如果打开事务出错,则关闭它
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
//最后清空错误上下文
ErrorContext.instance().reset();
}
}
我们看openSessionFromDataSource,因为前面我们解析主配置文件把所有的节点信息都保存在了Configuration对象中,所以一开始直接获得Environment节点的信息,这个节点配置了数据库连接和事务。后通过Environment创建了一个事务工厂,通过事务工厂实例化了一个事务对象。最后创建了一个执行器Executor,我们知道session是与数据库交互的顶层API,session中会维护一个executor来负责sql生产和执行和查询缓存等。
//产生执行器
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
//这句再做一下保护,囧,防止粗心大意的人将defaultExecutorType设成null?
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
//然后就是简单的3个分支,产生3种执行器BatchExecutor/ReuseExecutor/SimpleExecutor
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
//如果要求缓存,生成另一种CachingExecutor(默认就是有缓存),装饰者模式,所以默认都是返回CachingExecutor
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
//此处调用插件,通过插件可以改变Executor行为
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
这个过程是判断生成哪一种执行器的过程,mybatis执行器有三种:
/**
* 执行器的类型
*
*/
public enum ExecutorType {
//ExecutorType.SIMPLE
//这个执行器类型不做特殊的事情。它为每个语句的执行创建一个新的预处理语句。
//ExecutorType.REUSE
//这个执行器类型会复用预处理语句。
//ExecutorType.BATCH
//这个执行器会批量执行所有更新语句,如果SELECT在它们中间执行还会标定它们是必须的,来保证一个简单并易于理解的行为。
SIMPLE, REUSE, BATCH
}
SimpleExecutor: 简单执行器,是 MyBatis 中默认使用的执行器,每执行一次 update 或 select,就开启一个 Statement 对象,用完就直接关闭 Statement 对象(可以是 Statement 或者是 PreparedStatment 对象)
ReuseExecutor: 可重用执行器,这里的重用指的是重复使用 Statement,它会在内部使用一个 Map 把创建的 Statement 都缓存起来,每次执行 SQL 命令的时候,都会去判断是否存在基于该 SQL 的 Statement 对象,如果存在 Statement 对象并且对应的 connection 还没有关闭的情况下就继续使用之前的 Statement 对象,并将其缓存起来。
因为每一个 SqlSession 都有一个新的 Executor 对象,所以我们缓存在 ReuseExecutor 上的Statement 作用域是同一个 SqlSession。
BatchExecutor: 批处理执行器,用于将多个SQL一次性输出到数据库。
我们如果没有配置或者指定的话默认生成的就是SimpleExecutor。执行器生成完后返回了一个DefaultSqlSession,这里面维护了Configuration和Executor。
查询过程源码分析
/**
* Retrieve a single row mapped from the statement key and parameter.
* 根据指定的SqlID获取一条记录的封装对象,只不过这个方法容许我们可以给sql传递一些参数
* 一般在实际使用中,这个参数传递的是pojo,或者Map或者ImmutableMap
* @param <T> the returned object type
* @param statement Unique identifier matching the statement to use.
* @param parameter A parameter object to pass to the statement.
* @return Mapped object
*/
<T> T selectOne(String statement, Object parameter);
//核心selectOne
@Override
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
//转而去调用selectList,很简单的,如果得到0条则返回null,得到1条则返回1条,得到多条报TooManyResultsException错
// 特别需要主要的是当没有查询到结果的时候就会返回null。因此一般建议在mapper中编写resultType的时候使用包装类型
//而不是基本类型,比如推荐使用Integer而不是int。这样就可以避免NPE
List<T> list = this.<T>selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
找到实现类DefaultSqlSession,发现调用了selectList()方法,其实查询一个或者多个都是调用selectList方法,进入此方法:
@Override
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
//核心selectList
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//根据statement id找到对应的MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
//转而用执行器来查询结果,注意这里传入的ResultHandler是null
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
我们重点看:
//根据statement id找到对应的MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
调用selectOne查询时传的参数是sql的id值:selectUserById和sql的参数:1,在这行代码中参数statement的值就是selectUserById , 我们回忆一下,mybatis初始化的时候是不是把每个sql标签解析成一个个的MapperStatement,并且把这些MapperStatement装进configuration对象维护的一个Map集合中,这个Map集合的key值就是sql标签的id,value是对应的mapperstatement对象,我们之前说装进集合中备用就是在这里用的,这里用sql标签的id值从Map中取出对应的MapperStatement对象。
比如我们现在selectOne方法调用的的是selectUserById 这个sql,所以现在通过selectUserById 这个key值从configuration维护的Map中取出对应的MapperStatement对象。为什么要取出这个对象呢?因为mybatis把一个sql标签的所有数据都封装在了MapperStatement对象中。比如:出参类型,出参值,入参类型,入参值还有sql语句等等。
接着看下一行代码:
executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
MapperStatement被当做参数传入query方法,这个query方法是执行器调用的,我们知道执行器的作用是sql的生成执行和查询缓存等操作,在这个query方法中我们会查询缓存和执行sql语句,我们进入query方法
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
//query时传入一个cachekey参数
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
//被ResultLoader.selectList调用
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
//默认情况下是没有开启缓存的(二级缓存).要开启二级缓存,你需要在你的 SQL 映射文件中添加一行: <cache/>
//简单的说,就是先查CacheKey,查不到再委托给实际的执行器去查
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
一开始从MapperStatement中获取BoundSql这个对象,真正的sql语句封装在这个对象中,这个对象也负责把sql中的占位符替换成我们传的参数,只是MapperStatement维护了BoundSql的引用而已。
然后看createcachekey,意思就是根据这些参数生成一个缓存key,当我们调用同一个sql,并且传的参数是一样的时候,生成的缓存key是相同的。
一开始获取缓存,但是这个缓存并不是我们存储查询结果的地方,应该是二级缓存。它查询缓存为null,就会执行最后一句代码。
delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
最后一行代码通过调用delegate.query方法。delegate是一个executor的引用,在这里其实是simpleexecutor简单执行器的引用,我们知道获取一个会话session的时候会创建一个执行器,如果没有配置的话默认创建的就是simpleExecutor,在这把simpleExecutor的引用维护到cachingExcutor中。因此缓存执行器未能执行sql就交给simpleExecutor来执行。
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
//如果已经关闭,报错
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//先清局部缓存,再查询.但仅查询堆栈为0,才清。为了处理递归调用
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
//加一,这样递归调用到上面的时候就不会再清局部缓存了
queryStack++;
//先根据cachekey从localCache去查
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
//若查到localCache缓存,处理localOutputParameterCache
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//从数据库查
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
//清空堆栈
queryStack--;
}
if (queryStack == 0) {
//延迟加载队列中所有元素
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
//清空延迟加载队列
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
//如果是STATEMENT,清本地缓存
clearLocalCache();
}
}
return list;
}
一开始声明了一个集合list,然后通过我们之前创建的缓存key去本地缓存localCache中查询是否有缓存,下面判断,如果集合不是null就处理一下缓存数据直接返回list,如果没有缓存,他回从数据库中查,你看他们这名字起的一看就知道是什么意思queryFromDatabase,我们现在执行的是第一条selectOne,没有缓存我们进入queryFromDatabase方法。
//从数据库查
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
//先向缓存中放入占位符???
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
//最后删除占位符
localCache.removeObject(key);
}
//加入缓存
localCache.putObject(key, list);
//如果是存储过程,OUT参数也加入缓存
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
执行doQuery方法从数据中查到数据并放入缓存中。
//select
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
//新建一个StatementHandler
//这里看到ResultHandler传入了
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//准备语句
stmt = prepareStatement(handler, ms.getStatementLog());
//StatementHandler.query
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
doQuery方法一开始从Configuration中拿出会话处理器Statementhandler,作用是装了JDBC statement操作,负责对JDBC statement的操作。JDBC操作数据库的步骤通常是:注册驱动->获取连接->创建会话对象(上面提到的statement或者是可以防止注入攻击的preparestatement)->执行sql语句->处理结果集->关闭连接
获取会话处理器后,执行了prepareStatement方法
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
//调用StatementHandler.prepare
stmt = handler.prepare(connection);
//调用StatementHandler.parameterize
handler.parameterize(stmt);
return stmt;
}
最开始是getConnection获取数据库连接,然后执行handler.prepare();这个方法的作用就是根据连接事务创建会话对象。进入prepare方法
//准备语句
@Override
public Statement prepare(Connection connection) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = null;
try {
//实例化Statement
statement = instantiateStatement(connection);
//设置超时
setStatementTimeout(statement);
//设置读取条数
setFetchSize(statement);
return statement;
} catch (SQLException e) {
closeStatement(statement);
throw e;
} catch (Exception e) {
closeStatement(statement);
throw new ExecutorException("Error preparing statement. Cause: " + e, e);
}
}
重点看instantiateStatement:
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
//调用Connection.prepareStatement
String sql = boundSql.getSql();
if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
String[] keyColumnNames = mappedStatement.getKeyColumns();
if (keyColumnNames == null) {
return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
} else {
return connection.prepareStatement(sql, keyColumnNames);
}
} else if (mappedStatement.getResultSetType() != null) {
return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
} else {
return connection.prepareStatement(sql);
}
}
我们发现return值全部都是prepareStatement预编译会话对象,说明mybatis默认可以防止注入攻击。
返回来看prepareStatement方法
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
//调用StatementHandler.prepare
stmt = handler.prepare(connection);
//调用StatementHandler.parameterize
handler.parameterize(stmt);
return stmt;
}
会话对象获取完毕后,执行handler.parameterize(stmt)方法;这个执行的步骤基本跟获取会话对象的步骤一样
public void parameterize(Statement statement) throws SQLException {
//调用ParameterHandler.setParameters
parameterHandler.setParameters((PreparedStatement) statement);
}
这里用到了parameterHandler参数处理器:负责对用户传递的参数转换成JDBC Statement所对应的数据类型,就是把String转换成varchar之类数据库所用的类型。
到此为止,我们拿到了会话对象,JDBC参数也设置完毕,我们可以继续执行doQuery方法了:
//select
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
//新建一个StatementHandler
//这里看到ResultHandler传入了
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//准备语句
stmt = prepareStatement(handler, ms.getStatementLog());
//StatementHandler.query
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
拿到预编译的会话对象后直接执行query方法:
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.<E> handleResultSets(ps);
}
把会话对象转换成preparedStatement预编译的会话对象,然后用会话对象调用execute方法。这和jdbc使用方式一样,都是通过调用execute方法。
sql执行完了我们需要处理结果集,在return中用到了resultSetHandler,结果集处理器:作用是负责将JDBC返回的ResultSet结果集对象转换成List类型的集合,就是把我们从数据库中查到的数据转换成List类型,我们现在是selectOne方法,所以这个集合中只有一条数据。
到此就把一次查询的步骤说完了,其实说到底就是封装了jdbc操作数据库的步骤,最终还是和jdbc操作数据库的步骤一模一样。他的封装就是为了让我们可以更方便的传参和处理结果集。
这时候已经把查询出来的一条数据放在缓存中了,再次调用第二条查询语句的话,就不会操作数据库了,而是直接从缓存中拿这条数据。
新增 更新 删除 操作
步骤和查询一模一样,就是一开始通过sql标签的id值从configuration维护的Map集合中取出对应的MapperStatement对象,然后通过封装jdbc的形式执行这个sql。查询最后走的是SimpleExecutor的doQuery方法,而新增、删除、和更新最后走的是SimpleExecutor的doUpdate方法,因为mybatis认为新增、删除、和更新都是更新了数据库的操作,不信我们把doUpdated的代码贴出来
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
//新建一个StatementHandler
//这里看到ResultHandler传入的是null
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
//准备语句
stmt = prepareStatement(handler, ms.getStatementLog());
//StatementHandler.update
return handler.update(stmt);
} finally {
closeStatement(stmt);
}
}
除了最后一句handler.update,其余和doQuery代码一致。
到这里Mybatis的运行原理就到此为止了,欢迎大家批评指正。