Mybatis-设计原则

Scroll Down

概述

本文的主要目的是想说清楚Mybatis的设计结构。
首先,想要说清楚Mybatis的结构需要先明确两点:
1、JDBC是如何使用的。
2、Mybatis主要解决了哪些问题。
那么我们先从JDBC使用先开始说。

JDBC使用

	/**
     * JDBC编程六步
     * 第一步:注册驱动
     * 第二步:获取数据库连接
     * 第三步:获取数据库操作对象
     * 第四步:执行SQL语句
     * 第五步:处理查询结果集
     * 第六步:释放资源
     */
    public static void jdbcProgram() {
        // 定义数据库连接对象
        Connection connection = null;
        // 定义数据库操作对象
        Statement statement = null;
        // 定义处理结果集对象
        ResultSet resultSet = null;

        try {
            // 第一步:注册驱动,通过创建驱动对象告知JDBC,我们即将连接哪个厂商的数据库
            // 注册驱动(new com.mysql.jdbc.Driver()也是可以的)
            Driver driver = new com.mysql.cj.jdbc.Driver();
            DriverManager.registerDriver(driver);

            // 第二步:获取数据库连接
            // 注意:Connection连接对象不能随意创建,最后使用完需要手动关闭
            // url统一资源定位
            String url = "jdbc:mysql://ip:3306/test";
            String userName = "zhangsan";
            String passWord = "zhangsan@";
            connection = DriverManager.getConnection(url, userName, passWord);

            // 第三步:获取数据库操作对象
            // 一个连接对象,可以创建多个数据库操作对象
            statement = connection.createStatement();

            // 第四步:执行SQL语句
            // 注意:jdbc中,sql语句不需要以分好`;`结尾,当然,你写了也不会报错
            // 定义DQL语句,jdbc中,通常是写DML语句和DQL语句
            String sql = StringUtils.join("select * from `user` where userName = 'lisi'");

            // 将sql语句发送给数据库管理系统,数据库管理系统(DBMS)会编译并执行该sql语句,把查询结果集放到ResultSet结果集对象中
            resultSet = statement.executeQuery(sql);

            // 第五步:处理查询结果集
            // 处理查询结果集,一个Statement可以得出多个ResultSet
            // resultSet.next()方法作用:if the new current row is valid, return true. if there are no more rows, return false
            // 将光标向前移动一行,如果指向当前行有记录,则返回true,若指向当前行无记录,返回false
            while (resultSet.next()) {
                // 获取当前光标指向行中的数据(通过特定类型获取数据)
                // 该方式,明确知道字段的类型,可以节省类型转换花费的性能,该方式的程序更加健壮,性能更高,推荐使用
                int id = resultSet.getInt("id");
                String name = resultSet.getString("userName");
                String status = resultSet.getString("status");
                System.out.println("id:" + id + ",用户名:" + name + ",有效状态:" + status);
            }

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 第六步:释放资源,分别进行try catch
            try {
                if (resultSet != null) {
                    resultSet.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }

            try {
                if (statement != null) {
                    statement.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }

            try {
                if (connection != null) {
                    connection.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

    }

通过上面JDBC使用的代码我们可以总结出通过JDBC完成访问数据库执行SQL需要如下几步:
第一步:注册驱动
第二步:获取数据库连接
第三步:获取数据库操作对象
第四步:执行SQL语句
第五步:处理查询结果集
第六步:释放资源

我们先看第一步和第二步,JDBC中获取连接通常是使用DriverManager来获取的,而数据库连接,通常有两个功能,一个是实现事务,另一个通过获取Statement执行SQL语句。

所以基于上面的第一点,Mybatis抽象出了Transaction事务接口用于描述事务的相关能力并且对外提供。这个接口主要提供的能力:获取连接,提交,回滚,获取超时时间、关闭连接。有两类实现类,一个是支持传入Connection的事务,另一种是基于数据库连接池的事务。
首先,获取连接是事务的一个能力,对于一个ORM框架来说,数据库连接的获取可以归纳为两个途径:

  • 1、直接连接数据库的单条连接,这种连接方式可以是直接连接,使用完成后直接关闭即可。也可以是外部调用Mybatis框架传入的连接,这种连接的生命周期一般由外部系统管理,比如Spring的事务。
  • 2、使用数据库连接池管理的连接,这是一种池化能力。通过池化技术管理数据库连接,节约每次开启连接的开销。

注意:使用JDBC去操作数据库时,缺省情况下,autoCommit是false也就是默认开启手动提交事务。

所谓执行器,就是将自动化调节机构通过执行元件直接改变生产过程的参数,使生产过程满足预定的要求。通俗来讲就是一个信号通过执行器转换为另一个可以直接被执行的信号。那么,Mybatis里面的执行器的功能就是将配置文件的描述的信息转换成JDBC可以使用的信息。

Executor的功能职责和Statement Handler的功能职责是不一样的,需要明确。
Executor
image
Executor层抽象代表着不同的执行方式:比如简单的方式、可以复用Statement的方式以及批处理方式。同时Executor层还实现了一、二级缓存的功能。使用装饰模式,比如带有缓存功能的执行器。
Executor持有Transaction引用(数据库连接)并且每个方法的入参是MappedStatement(SQL描述信息)和ParameterObject(用户执行SQL参数)。那么我们实际上就可以通过Executor来完成Connection连接的获取,以及调用StatementHandler来完成Statement的实例化以及参数的绑定、执行。也就是说StatementHandler本身不具备执行一个SQL的能力,它只是对Statement的一些JDBC层面的一些封装,比如Statement的实例化,参数绑定,以及对应的SQL执行方法。而编排Statement的实例化以及参数的绑定、执行的能力是在Executor里面来做的,这才是Executor的核心功能,执行器。所谓的执行器就是调用底层的原子能力的封装,实现SQL的执行。Executor只是在上层对执行SQL模板方法调用的实现。

StatementHandler
image-1685540665336
StatementHandle层抽象代表着不同的SQL发送语句的方式:比如简单的方式,存储过程的方式以及预处理的方式,Statement的主要功能是执行SQL语句。

这两层可以根据不同的场景组合使用。
StatementHandler的主要功能是封装Statement的构建,获取SQL,参数的绑定,执行,以及返回结果的映射,后面的两点主要是通过回调TypeHandler来实现的。
这里需要强调下SQL执行的三个关键的要素,只要有这三个要素,那么我们就可以执行一个SQL了,具体是什么呢?
1、Statement:这个连接的要素体现在Executor在创建的时候会持有一个Transaction的引用(Transaction引用就是Connection),通过Connection我们就能获取一个Statement对象了。
2、SQL:上层接口在调用Executor方法的时候会根据不同的方法和statementID来选择不同的SQL,那么这个时候就需要MappedStatement对象了,这个对象的主要作用实际上就是描述一个需要执行的SQL,以及映射相关信息。
3、param:用户的参数,也就是WHERE后面的一些信息。这部分信息也在入参传入。
那么此时Executor的核心结构就很清晰了。

SqlSession可以为API,Executor和Statement可以为SPI。
Mybatis中SqlSession是服务域,Executor是实体域。但是在Mybatis中会话域其实没有具体化,它被分成了两部分,一部分是用户的入参,另一部分是配置文件中的SQL的模型。
把元信息交由实体域持有,把一次请求中的临时状态由会话域持有,由服务域贯穿整个过程。

难以理解抽象类和接口

难以理解抽象类和接口的人常常使用具体的角色来编程,而不使用接口,它们总想使用具体的类来解决所有的问题。但是如果只使用具体的类来解决问题,很容易导致类之间的强耦合,这些类也难以作为组件被再次利用。为了弱化类之间的耦合,进而使得类更加容易作为组件被再次利用,我们需要引入抽象类和接口。

扩展信息:Interceptor为什么不使用装饰模式,装饰模式与动态代理模式相比有个弊端就是装饰模式需要手动装饰所有需要装饰的方法。