在初始化阶段结束之后,我们来对读写阶段进行追踪,初步探究当进行一次数据库的读或写操作时,MyBatis内部都要经过哪些步骤。

获得 SqlSession

在初始化阶段,我们已经获得了 SqlSessionFactory,而数据库操作过程中需要一个SqlSession对象。从类的名称就可以看出,SqlSession是由 SqlSessionFactory生成的。在主方法中,由 SqlSessionFactory生成 SqlSession的过程如下面代码所示:
image.png

进入 _SqlSessionFactory 的实现类,_DefaultSqlSessionFactory#openSessionFromDataSource:
image.png
此处是生成 SqlSession的核心源码。

进入 DefaultSqlSession 类,可以看到它提供了查询、增加、更新、删除、提交、回滚等大量的方法:
image.png

从DefaultSqlSession 返回后,主方法中“SqlSessionsession=sqlSessionFactory.openSession()”这句代码就执行完毕了。

映射接口文件与映射文件的绑定

映射接口文件是指UserMapper.class等存有接口的文件,而映射文件是指UserMapper.xml等存有 SQL操作语句的文件。最终,MyBatis将这两类文件一一对应了起来。
在进行数据查询之前,主方法先通过下面的代码找到UserMapper接口对应的实现:
image.png

该操作通过 Configuration类的 getMapper方法转接,最终进入 MapperRegistry类中的getMapper方法。MapperRegistry类中的 getMapper方法如代码所示:
image.png

映射接口的代理

我们已经知道“session.getMapper(UserMapper.class)”方法最终得到的是“mapperProxy Factory.newInstance(sqlSession)”返回的对象。那该对象到底是什么呢?
我们追踪“mapperProxyFactory.newInstance(sqlSession)”方法,可以在 MapperProxyFactory类中找到代码所示的方法:
image.png

基于反射的动态代理对象建立后,被代理对象的方法会被代理对象的 invoke 方法拦截:
image.png
接下来主方法中代码会进入下面的方法:
image.png

然后,会触发 MapperMethod#execute:
image.png

示例代码中是查询操作,所以就会进入 MapperMethod#executeForMany:
image.png
追踪到这里,MyBatis 已经完成了为映射接口注入实现的过程。于是,对映射接口中抽象方法的调用转变为了数据查询操作。

SQL 语句的查找

所示的操作调用到了 DefaultSqlSession#selectList:
image.png

查询结果缓存

对应的数据库操作节点被查找到后,MyBatis 使用执行器开始执行语句。在上一段代码中可以看到触发操作:
image.png
上述 query方法实际是一个 Executor接口中的抽象方法,如代码所示:
image.png

该抽象方法有两种实现:
image.png
分别在BaseExecutor类和CachingExecutor类中。
在执行代码示例时,会发现实际执行的是 CachingExecutor#query :
image.png
接下来流程会走到 CachingExecutor#query ,看是从缓存中获取数据,还是要重新查一遍:
image.png

数据库查询

在上一步的代码中,delegate调用的 query方法再次调用了一个Executor接口中的抽象方法,如下面代码所示。我们同样在该抽象方法上打断点以追踪程序的实际流向:
image.png
进入 BaseExecutor# query方法,开始查询数据库:
image.png

BaseExecutor#queryFromDatabase :
image.png
doQuery方法是 BaseExecutor类中的抽象方法,实际运行的最终实现如下面代码所示:
image.png
在最后调用了 StatementHandler#query :
image.png
image.png
image.png

此处的 指向 PreparedStatementHandler#query:
image.png
因为 PreparedStatement类并不是MyBatis中的类,因而ps.execute()的执行不再由MyBatis负责,而是由 com.mysql.cj.jdbc包中的类负责,所以此处就不再继续追踪了。

此处以使用 MySQL 数据库为例,若使用其他数据库,则负责执行 ps.execute() 的包会不同:
image.png
查询完成之后的结果放在 PreparedStatement对象中,通过调试工具可以看到其中包含了这次查询得到的数据库字段信息、数据记录信息等:
待补…

这一步数据库查询操作涉及的方法较多。整个流程的关键步骤如下:

  • 在进行数据库查询前,先查询缓存;如果确实需要查询数据库,则数据库查询之后的结果也放入缓存中。
  • SQL 语句的执行经过了层层转化,依次经过了MappedStatement 对象、Statement对象和 PreparedStatement对象,最后才得以执行。·
  • 最终数据库查询得到的结果交给 ResultHandler对象处理。

处理结果集

查询得到的结果并没有直接返回,而是交给 ResultHandler对象处理。ResultHandler是结果处理器,用来接收此次查询结果的方法是该接口中的抽象方法 handleResultSets,如代码所示:
image.png
此接口只有一个实现方法 DefaultResultSetHandler#handleResultSets:
image.png
在上述方法中,查询出来的结果被遍历后放入了列表multipleResults 中并返回。

Mybatis 如何将数据库输出记录转化为对象列表

在 DefaultResultSetHandler 中进行了多次跳转,整个方法的调用链路如图所示:
image.png

其中重点关注的是图中粗线边框标注的三个方法。

  • createResultObject(ResultSetWrapper,ResultMap,List<Class<?>>,List<Object>,String)方法:该方法创建了输出结果对象。在示例中,为 User对象。

image.png
该方法根据输出对象的不同,使用类型处理器或通过调用构造方法等方式创建输出结果对象。

  • applyAutomaticMappings 方法:在自动属性映射功能开启的情况下,该方法将数据记录的值赋给输出结果对象。

image.png

  • applyPropertyMappings方法:该方法按照用户的映射设置,给输出结果对象的属性赋值。

image.png

经过以上过程,MyBatis将数据库输出的记录转化为了对象列表。

之后,以上方法逐级返回。最后,装载着对象列表的multipleResults 被返回给“List<User> userList”变量,我们便拿到了查询结果。追踪到这里,主方法中代码所示的语句终于执行完成了:
image.png

总结

在整个数据库操作阶段,MyBatis完成的工作可以概述为以下几条:

  • 建立连接数据库的 SqlSession。· 查找当前映射接口中抽象方法对应的数据库操作节点,根据该节点生成接口的实现。
  • 接口的实现拦截对映射接口中抽象方法的调用,并将其转化为数据查询操作。
  • 对数据库操作节点中的数据库操作语句进行多次处理,最终得到标准的 SQL语句。
  • 尝试从缓存中查找操作结果,如果找到则返回;如果找不到则继续从数据库中查询。
  • 从数据库中查询结果。
  • 处理结果集。
    • 建立输出对象;
    • 根据输出结果对输出对象的属性赋值。
  • 在缓存中记录查询结果。
  • 返回查询结果。

通过以上步骤可以看出,MyBatis完成一次数据库操作的过程还是十分复杂的。因此,平时的软件开发过程中要尽量减少数据库操作,这样能极大地提高软件运行的效率。