mybatis原理之手写mybatis框架(一)架构和流程分析

本章希望通过手写mybatis框架的核心部分,帮助加深对mybatis框架底层原理的理解,主要包括框架的执行流程,动态sql的解析流程,框架中各个组件和类的作用,以及他们之间是如何交互的。
手写之前,我们需要对mybatis的架构和流程有大致的掌握。

1. mybatis的架构和流程分析

先直接来一张mybatis的架构图,看下mybatis按照架构整体是怎么设计的:
在这里插入图片描述

  • 接口层
    接口层是MyBatis提供给开发人员的一套API,主要表现在SqlSession接口,通过SqlSession接口开发人员就可以完成对数据库CRUD。这层主要考虑的就是使用人的需求,用起来怎么方便接口就怎么设计,属于对外的接口。

  • 数据处理层
    数据处理层就是具体解析和执行sql的地方,是MyBatis框架内部的核心,底层最终还是调用JDBC代码来执行的。这层就是通过对JDBC执行sql的步骤进行了拆解,划分了不同模块,同时抽象出了很多组件,每个组件各司其职,通过协作让整个sql执行更加灵活。
    主要责任:
    (1)参数的解析与绑定
    (2)SQL的解析
    (3)SQL的执行
    (4)结果集映射的解析与处理

  • 基础支撑层
    支撑层用来支撑整个框架运行的,主要负责:
    (1)对数据库连接的支持与管理
    (2)对事务的支持与管理
    (3)对配置信息的支持与管理
    (4)对查询缓存的支持与管理


接着我们看下执行流程图,看下配置文件是如何结合Mybatis中各个组件进行整个程序执行的。
在这里插入图片描述

说明

  • 1.mybatis配置文件
    所有的配置文件在框架运行的一开始就会被解析,并将所有的配置信息封装到Configuration对象中。

    • SqlMapConfig.xml,全局配置文件,配置了mybatis的运行环境等信息。
    • Mapper.xml,映射文件,配置了sql相关信息。
  • 2.SqlSessionFactory
    通过mybaris环境等配置信息构造SqlSessionFactory,即会话工厂。(创建过程主要是通过SqlSessionFactoryBuilder对象加载配置文件,将解析的配置信息封装成Configuration对象后,再将Configuration对象传递给SqlSessionFactory对象完成构建)

  • 3.sqlSession
    通过上面的会话工厂创建sqlSession会话,程序员就是通过sqlsession会话接口对数据库进行增删改查操作。这个是mybatis对外提供的操作接口。

  • 4.Executor执行器
    mybatis底层定义了Executor执行器接口来具体操作数据库, 而上面的对外接口sqlsession底层就是通过executor接口操作数据库的。Executor接口有两个实现,一个是基本执行器BaseExecutor(是一个抽象类,采用模板方法设计模式,基本执行器下面又分为简单执行器、可重用执行器、批量执行器…)、一个是缓存执行器CachingExecutor,Executor是mybatis内部的执行接口,四大组件之一。
    (四大组件:Exucutor、StatementHandler、ParameterHandler、ResultSetHandler,各司其职,让SQL执行流程更加灵活)

  • 5.MappedStatement
    它也是mybatis一个底层封装对象,它包装了mybatis配置信息及sql映射信等。映射文件中每一个select\insert\update\delete标签都对应一个Mapped Statement对象,select\insert\update\delete标签的id即是Mapped statement的id,此外还封装了标签中的sql语句(SqlSource),参数类型,结果集类型,statment类型等。(JDBC中,有三种Statement类型,Statement,PreparedStatment,CallableStatment)

    • MappedStatement对sql执行的输入参数进行定义,支持HashMap、基本类型、pojo,Executor在MappedStatemen执行sql前将输入的java对象映射至sql中,输入参数映射过程相当于jdbc编程中对preparedStatement设置参数。

    • MappedStatement对sql执行的输出结果进行定义,支持HashMap、基本类型、pojo,Executor在MappedStatemen执行sql后将输出结果映射至java对象中,输出结果映射过程相当于jdbc编程中对结果的解析处理过程。

在这里插入图片描述


接下来看第三张图,主要讲解的是SqlSession水平面下的调用过程,展示了整个调用过程中各个组件的职责,以及组件间是如何交互,如何和JDBC的代码进行交互的:
在这里插入图片描述

  • SqlSession:可以看到SqlSession作为MyBatis工作的顶层API接口,是对外提供的操作接口,而底层是调用Executor执行器。

  • Executor:执行器是Mybatis的核心,是内部的执行接口,同时还提供了查询缓存的功能。

  • SqlSource:MappedStatment中的SqlSource具有解析sql的功能,通过SqlSource可以并获得BoundSql,它维护了可执行的Sql语句和对应的参数映射信息。

  • StatementHandler:StatementHandler是用来处理和JDBC中的Statement进行交互的,例如对statment设置入参、执行查询、执行更新、结果集封装等。

  • ParameterHandler:StatementHandler对statment设置入参,底层主要依靠这个接口。

  • ResultSetHandler:StatementHandler对statment的结果集封装,底层主要依靠这个接口。

通过上面的分析,我们进行一个总结:

  • SqlSession
    mybatis对外提供的程序员使用的接口,通过接口完成CRUD
  • Executor
    执行器,mybatis内部执行的接口,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
  • StatementHandler
    封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。
  • ParameterHandler
    负责对用户传递的参数转换成JDBC Statement 所需要的参数
  • ResultSetHandler
    负责将JDBC返回的ResultSet结果集对象转换成List类型的集合
  • TypeHandler
    负责java数据类型和jdbc数据类型之间的映射和转换
  • MappedStatement
    维护了一条<select|update|delete|insert>节点的封装
  • SqlSource
    负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回BoundSql,表示动态生成的SQL语句以及相应的参数信息
  • Configuration
    MyBatis所有的配置信息都维持在Configuration对象之中。

2.Mybatis中的SQL解析

上面已经提到过SqlSource具有解析sql的功能,那么解析流程到底是什么样的呢?我们举个例子:
在这里插入图片描述
之前说过MappedStatement对象维护了一条<select|update|delete|insert>节点的封装,上面的例子中,整个<select>就对应一个MappedStatement对象,里面维护了id,parameterType,resultType,statmentType和标签内的sql语句。

那么标签内的sql语句具体是以什么样的数据结构存储的呢?
首先分析一下<select>标签,对于xml来说,它是一个混合元素,可以包含子标签,并且子标签之间可以穿插文本内容,dom解析<select>标签就可以看成一个Node节点,它有很多子节点,子节点中既有文本节点,也有元素节点,元素节点又有可能有子节点…而在mybatis中把这样的节点,定义成了【SqlNode】,直译为Sql节点。【SqlNode】它是一个接口,按照节点包含的内容不同,实现类分为:

  • StaticTextSqlNode:封装的是仅带有#{}的文本节点
  • TextSqlNode:封装的是带有${}的文本节点
  • IfSqlNode:封装的是if动态标签的混合节点
  • MixedSqlNode:混合节点,代表一组节点。使用组合设计模式,以集合方式存储子节点,使得用户可以使用一致的方法操作单个对象和组合对象。

所以【SqlNode】主要就是用来存储<select>标签下一个个sql节点的。除了存储以外,<select>标签所代表的sql语句还需要根据用户传递的参数对其包含的一个个sql节点进行选择拼接,最终拼成一个完整的sql语句。

因此【SqlNode】接口定义了一个方法apply(DynamicContext context),其中DynamicContext里面维护了用户传递的入参,和一个StringBuilder变量,用来存储拼接的sql语句,该方法会根据当前节点的逻辑对节点中的信息进行处理,然后拼接到StringBuilder中,注意此时的处理,只是根据动态标签的逻辑,完成了字符串的拼接,或者替换了字符串中的${},并不会处理#{}。当调用顶层节点的apply方法的时候,会依次遍历树中每个节点,所以每个节点都有机会并且按照顺序依次执行自己的apply方法对StringBuilder进行拼接,最终从StringBuilder中方可以得到一个完整的sql语句,当然得到的是一个最多只包含#{}的sql语句。

上面的过程我们可以称为拼接过程

只包含#{}的sql语句,还不能被JDBC直接执行,所以mybatis定义了一个SqlSource接口,其实现类会包含SqlNode,并提供了一个获取可执行SQL的方法getBoundSql(Object parameterObject),该方法底层实现,会调用解析器sqlSourceParser,它会根据用户传递的入参,将SqlNode拼接完整的语句进行一次解析,把#{}替换为?占位符,同时将#{}中的参数信息封装成了【ParameterMapping】对象,组成一个集合按照顺序作为该sql语句占位符对应的参数映射信息,最后将sql和参数信息封装到了【BoundSql】对象返回。

这个过程我们可以称为解析过程

根据【SqlSource】实现类中包含的【SqlNode】类型不同,也有不同实现类:

  • DynamicSqlSource:包含了带有${}或者动态SQL标签的SqlNode
  • RawSqlSource:仅包含只带有#{}的SqlNode

为什么这么分?因为只包含#{}的sql语句,是不需要经历拼接过程的(没有${}或者动态标签需要处理),只需要解析一次以后,就可以使用占位符来长期使用。而${}和动态标签的sql,每次用户传递的入参不同,整个语句拼接后的结果都不一样,所以每次调用都需要重新拼接并解析。
所以往往RawSqlSource在构造的时候拼接解析就可以了,而DynamicSqlSource要在每次调用方法的时候重新拼接在解析

  • StaticSqlSource:上面两个SqlSource解析完成以后的结果都会封装到该对象中,这里面的sql是可以直接使用的。
总结SQL解析:

SQL解析可以分成两个阶段:

  • 拼接阶段:拼接sql节点,只处理动态标签和${},最终返回只包含#{}的sql语句
  • 解析阶段:处理#{},解析成占位符,最终返回可执行的sql和参数映射信息

涉及的接口和类:

  • SqlSource接口:
    第一、实现类要封装未解析的SQL信息(SqlNode集合)
    第二、要提供对未解析的SQL信息,进行解析的功能

    • DynamicSqlSource:封装的SqlNode集合中含有"带有${}或者动态SQL标签的SqlNode"
      解析工作是发生在每一次调用getBoundSql方法的时候
    • RawSqlSource:封装的SqlNode集合中只含有"只带有#{}的SqlNode"
      解析工作是发生在第一次构造RawSqlSource的时候,只需要被解析一次
    • StaticSqlSource:当上面两个SqlSource解析后,都会把解析结果封装到该对象中,通过该SqlSource获取到BoundSql
  • SqlNode接口:
    1、封装SQL节点信息
    2、提供对封装的SQL节点的拼接解析功能,此处解析指替换#{}

    • MixedSqlNode:主要起到封装集合节点作用,并且进行一些操作,代表的含有子节点的节点
    • TestSqlNode:封装的是带有${}的文本字符串,并提供根据参数替换掉${}功能
    • StaticTextSqlNode:封装的是带有#{}的文本字符串,不用特殊处理
    • IfSqlNode:封装的是if动态标签的文本字符串,根据参数决定是否拼接

    • 元素:我们认为动态标签和${}都是字符串拼接的操作,对于这种操作都认为是动态的sql信息,#{}或者没有占位符都属于静态,对应sql处理"拼接"和"解析"两个流程
  • DynamicContext:动态上下文,就是封装的入参信息,解析过程中的SQL信息

  • BoundSql:封装解析之后可以执行的sql语句,以及解析占位符?时产生的参数映射信息

  • ParameterMapping:从#{}中解析出来的参数信息,包括参数名称和类型

SqlSourece、SqlNode、BoundSql这几个接口在整个解析阶段和执行阶段都非常重要!也是最难理解的部分。

3.分析JDBC的执行步骤

mybatis底层都是对JDBC的封装,接下来我们在JDBC的执行步骤基础上简单分析一下如何手写mybatis,主要以面向过程方式分析:

	@Test
    public void test() {
        Connection conn = null;
        PreparedStatement stmt = null;
        ResultSet rs = null;

        try {
            // 1 注册驱动
            Class.forName("com.mysql.jdbc.Driver");
            // 2 获得连接
            String url = "jdbc:mysql://rm-bp......nm1ao.mysql.rds.aliyuncs.com:3306/yth";
            conn = DriverManager.getConnection(url, "xiaoy1995", "Xiaoy9502");
            // 3 获取sql语句
            String sql = "select * from user where name = ?";
            // 4 获取预处理 statement
            stmt = conn.prepareStatement(sql);
            // 5 设置参数,序号从1开始
            stmt.setString(1, "刘备");
            // 6 执行SQL语句
            rs = stmt.executeQuery();
            // 7 处理结果集
            while (rs.next()) {
                // 获得一行数据
                System.out.println(rs.getString("name") + ", " + rs.getString("gender") + "," + rs.getString("phone")
                    + "," + rs.getString("address"));
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 8释放资源
            if (rs != null) {
                try {
                    rs.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (stmt != null) {
                try {
                    stmt.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

使用JDBC方式编程,缺点很多就不说了。我们直接思考可以从哪些方面进行优化。

  1. 首先里面的数据源配置信息和sql信息是经常变化的,可以放到配置文件中
    在这里插入图片描述
  2. 每次JDBC执行SQL的步骤都是相似的,可以对各步骤所代表的功能进行分析,划分不同模块,抽象出通用的组件。

针对上面两点,我们可以把JDBC按照下面两个流程进行分析

  • 解析流程
  • 执行流程
解析流程(从配置文件中获取JDBC需要的数据信息)

1.加载全局配置文件,最终将解析出来的信息,封装到【Configuration】对象中

  • a)解析运行时环境信息,这边主要指的是DataSource的配置信息,然后将DataSource封装到Configuration对象中存储。
  • b)当加载到mappers元素的时候,就会触发映射文件的加载。
    • 映射文件的加载,先解析select/update/insert/delete标签,获取id值(statementId)、SQL语句、参数类型、结果类型、statement类型,最终将解析出来的信息,封装到【MappedStatement】对象中,再将该对象封装到Map集合中,key为statementId,value就是该对象,然后将map集合存储到【Configuration】对象中
      • MappedStatement对象中存储Sql信息,是通过【SqlSource】进行存储的。SqlSource对象,不只是存储Sql信息,而且还提供对存储的SQL信息进行处理的功能。
      • SqlSource是通过一个SqlNode集合数据来封装的SQL信息。
    • 解析映射文件其他元素…
执行流程(使用JDBC代码从配置文件中读取SQL相关信息完成CRUD)

1.获取连接(需要driverclassname、url、username、password)

  • a)获取Configuration对象,从该对象中获取DataSource对象
  • b)有了DataSource对象,就可以从该对象中获取Connection对象

对应的JDBC执行步骤:

 // 1 注册驱动
Class.forName("com.mysql.jdbc.Driver");
// 2 获得连接
String url = "jdbc:mysql://rm-bp186l387tovm7nm1ao.mysql.rds.aliyuncs.com:3306/yth";
conn = DriverManager.getConnection(url, "xiaoy1995", "Xiaoy9502");

2.获取JDBC可以执行的SQL语句

  • 其实此处获取的是BoundSql,就是调用的【SqlSource】的SQL解析功能得到的
  • 从BoundSql中获取SQL语句

对应的JDBC执行步骤:

// 3 获取sql语句
String sql = "select * from user where name = ?";

3.获取statement对象

  • 从MappedStatement对象中获取statement的类型:simple、prepared、callable根据不同类型去创建不同的Statement对象

对应的JDBC执行步骤:

// 4 获取预处理 statement
stmt = conn.prepareStatement(sql);

4.从BoundSql中获取参数映射信息List<ParameterMapping>

  • 遍历给参数赋值,先需要读取ParameterMapping中的属性名称,再从入参对象中获取指定属性的值
  • 调用JDBC代码,完成属性设置

对应的JDBC执行步骤:

// 5 设置参数,序号从1开始
stmt.setString(1, "刘备");

5.执行statemement,并获取结果集ResultSet
对应的JDBC执行步骤:

// 6 执行SQL语句
rs = stmt.executeQuery();

6.处理结果集

  • 从MappedStatement对象中获取输出结果类型,也就是结果要映射的类型
  • 遍历结果集,获取结果集中每一行的数据
  • 获取结果集中每一行每一列的数据,获取列的名称
  • 根据列的名称,通过反射,去设置要映射的java类型的指定属性值

对应的JDBC执行步骤:

// 7 处理结果集
while (rs.next()) {
	// 获得一行数据
	System.out.println(rs.getString("name") + ", " +.....);
}

猜你喜欢

转载自blog.csdn.net/weixin_41947378/article/details/104155915