MyBatis运行原理

这篇文章是自己工作中记录下来的,由于工作比较忙,一直留存于本地忘了传到自己的博客中,现在优化了一下文章结构传上来,请各位看官批评指正。
另外,后续会有很多自己工作中记录的知识点文章做二次优化传上来。

在开发大数据平台调度系统过程中,我们通过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.");
          }
        }
      }
    }
  }
  1. 这方法开始是一个循环。因为一个mappers节点下面可能会有很多mapper节点。在应用中肯定不止一个mapper.xml。所以他会去遍历每一个mappers节点去解析该节点所映射的xml文件。
  2. 循环下面是一个if else判断。它先判断mappers下面的子节点是不是package节点。因为在实际开发中很多的xml文件,不可能每一个xml文件都用一个mapper节点去映射,我们直接用一个package节点去映射一个包下面的所有的xml,这是多文件映射。
  3. 如果不是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();
  1. 第一行代码是说实例化一个错误上下文对象。这个对象的作用就是把错误信息封装起来,如果出现错误就会调用这个对象的toString方法。此方法的resource参数就是String类型的xml名字,对应的就是mapper的xml文件。提示使用者错误发生在哪个xml文件中。
  2. 第二行表示读取这个xml获取输入流对象。
  3. 然后创建一个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;
  }

我们只看最后三行代码:

  1. MappedStatement statement = statementBuilder.build(); 通过解析出的参数构建一个MapperStatement对象
  2. 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);
        }
      }
    }
  }
  1. 首先获取名称空间,名称空间一般都是mapper的全限定名,通过反射获取这个mapper的class对象。
  2. if判断中,Configuration中也维护了一个Map对象,key值是我们刚才通过反射生产的mapper的class对象,value值是通过动态代理生产的class对象的代理对象。
  3. 因为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根据主配置文件初始化就完成了。总结来说就是:

  1. 解析主配置文件,将主配置文件中的所有信息封装到Configuration对象中。
  2. 详细来说就是通过XmlConfigBuilder解析主配置文件,通过XmlMapperBuild解析mappers下映射的所有xml文件(循环解析)。把每个xml中的各个sql解析成一个个MapperStatement对象,装在Configuration维护的一个Map集合中,key是id,value是MapperStatement对象。然后把解析过的xml的名字和名称空间装在set集合中,通过名称空间反射生成的mapper的class对象以及class对象的代理对象装在Configuration对象维护的mapperRegistry中的Map中。
  3. 我们用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的运行原理就到此为止了,欢迎大家批评指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值