Mybatis系列之核心分析

发布于:2023-09-14 ⋅ 阅读:(59) ⋅ 点赞:(0)



一、Mybatis的前世

Mybatis的前世:JDBC数据库编程

1、简述:

Mybatis是一个优秀的持久层框架。它对JDBC的操作数据库的过程进行封装。JDBC是连接数据库和Java程序的桥梁,通过JDBC API可以方便地实现对各种主流数据库的操作。

2、什么是JDBC:

Java数据库连接Java Database Connectivity,简称JDBC,是Java语言中用来规范客户端程序如何来访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法。JDBC是一类接口,制定了统一访问各类关系数据库的标准接口。如果没有JDBC这个接口标准的存在,程序员面对各类数据库的操作将会变得十分复杂。

3、什么是驱动:

JDBC是接口,驱动是接口的实现类,没有驱动将无法完成数据库连接,从而不能操作数据库。每个数据库厂商都需要提供自己的驱动,用来连接自己公司的数据库,也就是说驱动一般都是由数据库生成厂商提供。
驱动程序可以保证两个设备进行通信,它需要满足一定通信数据格式,数据格式有设备提供商规定,设备提供商为设备提供驱动软件,通过驱动程序可以与设备进行通信。

4、JDBC的开发步骤:

我们需要访问数据库时,首先要加载数据库驱动,只需加载一次,然后在每次访问数据库时创建一个Connection实例,获取数据库连接;获取数据库连接后,执行对应SQL,最后完成数据库操作时释放与数据库之间的连接。具体开发步骤如下:

《1》注册和加载数据库驱动

此步骤的目的是告知JVM使用的是哪一个数据库的驱动。Java加载数据库驱动的方法是调用class类的静态方法forName(),语法格式如下:
Class.forName(String driverManager)
例如,加载MySQL数据库驱动如下:
在这里插入图片描述

如果加载成功,会将加载的驱动类注册给DriverManager;如果加载失败,会抛出ClassNotFoundException异常。需要注意的是,要在项目中导入mysql-connection-java的jar包,方法是在项目中建立lib目录,在其下放入jar包。

《2》获得数据库连接

此步骤需要使用JDBC中的类,完成对数据库的连接。加载完数据库驱动后,就可以建立数据库的连接了,需要使用DriverManager类的静态方法getConnection()方法来实现。代码如下:
在这里插入图片描述

《3》获得语句执行对象,然后执行SQL语句,获取执行结果,最后释放资源

通过连接对象获取对SQL语句的执行者对象,利用执行者对象,向数据库发送并执行sql语句,然后获取到数据库的执行后的结果

建立了连接之后,就可以使用Connection接口的createStatement()方法来获取Statement对象,也可以调用prepareStatement()方法获得PrepareStatement对象,通过executeUpdate()方法来执行SQL语句。

以插入为例,我们可以使用Statement接口中的executeUpdate()方法,如下:
在这里插入图片描述

还可以使用PreparedStatement接口中的executeUpdate()方法,如下:
在这里插入图片描述


二、Mybatis的核心组成

SqlSessionFactoryBuilder:

根据配置或者代码生成SqlSessionFactory,采用的是分步构建的Builder模式

SqlSessionFactory:

SqlSessionFactory 是 MyBatis 的核心组件之一,它是应用程序与 MyBatis 数据库之间的一个交互对象。SqlSessionFactory 依据配置文件以及 Java API 的方式生成 SqlSession 对象,SqlSession 对象为执行 SQL 命令提供了相关接口。SqlSessionFactory 是 SqlSession 的工厂类,SqlSessionFactory 采用工厂模式设计,实现了 MyBatis 应用程序与数据库之间的解耦。

SqlSession(会话):

SqlSession 是 MyBatis 的核心组件之一,它是 Session(会话)级别的缓存,用于与数据库进行交互。SqlSession 对象提供了一系列操作数据库的 API,包括查询、插入、更新和删除数据等操作。SqlSession 作为 MyBatis 应用程序与数据库之间沟通的桥梁,SqlSession 可以被应用程序的各个层访问。

SQL Mapper(映射器):

Mapper 是 MyBatis 中抽象出来的一个概念,表示一类 DAO 类的接口。每个 Mapper 接口中定义了对应 SQL 操作的方法。Mapper 接口中的方法会被 MyBatis 解析成 MappedStatement 对象,与该 SQL 语句对应。Mapper 接口的定义使得应用程序开发者可以在无需编写具体的 SQL 语句的情况下,对数据库进行操作。

MappedStatement:

MappedStatement 是 MyBatis 用于存储 SQL 语句、入参、出参等相关信息的核心组件。在 MyBatis 中,Mapper 接口中的每个方法都会被解析成一个 MappedStatement 对象。MappedStatement 对象是一个有状态(stateful)对象,包含了 SQL 语句的语法、入参映射、结果映射等相关信息。

Executor:

Executor 是 MyBatis 中的核心组件之一,它主要负责查询语句的执行和结果的返回。Executor 的实现类有三种:SimpleExecutor、ReuseExecutor、BatchExecutor,分别对应于简单执行器、重复执行器和批处理执行器。Executor 提供了追踪和缓存查询结果的功能,能够提高执行效率。

以上这些组件相互配合,实现了 MyBatis 框架的核心功能,为开发者提供了便捷、高效的数据库操作方式,避免了手写 SQL 语句和 JDBC 操作的繁琐。


三、Mybatis的工作原理

1、MyBatis内部核心流程

在这里插入图片描述
图中流程就是MyBatis内部核心流程,每一步流程的详细说明如下文所述:

(1)读取MyBatis的配置文件。mybatis-config.xml为MyBatis的全局配置文件,用于配置数据库连接信息。

(2)加载映射文件。映射文件即SQL映射文件,该文件中配置了操作数据库的SQL语句,需要在MyBatis配置文件mybatis-config.xml中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。

(3)构造会话工厂。通过MyBatis的环境配置信息构建会话工厂SqlSessionFactory。

(4)创建会话对象。由会话工厂创建SqlSession对象,该对象中包含了执行SQL语句的所有方法。

(5)Executor执行器。MyBatis底层定义了一个Executor接口来操作数据库,它将根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。

(6)MappedStatement对象。在Executor接口的执行方法中有一个MappedStatement类型的参数,该参数是对映射信息的封装,用于存储要映射的SQL语句的id、参数等信息。

(7)输入参数映射。输入参数类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输入参数映射过程类似于JDBC对preparedStatement对象设置参数的过程。

(8)输出结果映射。输出结果类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输出结果映射过程类似于JDBC对结果集的解析过程。

2、Mybatis层次结构

在这里插入图片描述
SqlSession:作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能

Executor:MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护

StatementHandler :封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。

ParameterHandler: 负责对用户传递的参数转换成JDBC Statement 所需要的参数,

ResultSetHandler:负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;

TypeHandler :负责java数据类型和jdbc数据类型之间的映射和转换

MappedStatement: MappedStatement维护了一条<select|update|delete|insert>节点的封装,

SqlSource:负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回

BoundSql :表示动态生成的SQL语句以及相应的参数信息

Configuration:MyBatis所有的配置信息都维持在Configuration对象之中。


四、Mybatis的插件机制

插件是一种常见的扩展方式,大多数开源框架也都支持用户通过添加自定义插件的方式来扩展或者改变原有的功能,MyBatis中也提供的有插件,虽然叫插件,但是实际上是通过拦截器(Interceptor)实现的,在MyBatis的插件模块中涉及到责任链模式和JDK动态代理,这两种设计模式的技术知识也是大家要提前掌握的。

将从以下4点介绍:

  1. 自定义插件
    1.1 认识拦截器
    1.2 创建Interceptor实现类
    1.3 MyBatis拦截器注册的三种方式
  2. 插件实现原理
    2.1 初始化操作
    2.2 如何创建代理对象
    2.3 执行流程
    2.4 多拦截器
  3. PageHelper分析
    1.1 PageHelper的应用
    1.2 配置拦截器
  4. 应用场景分析

1. 自定义插件

1.1 拦截器(Interceptor)

Mybatis拦截器设计的初衷就是为了供用户在某些时候可以实现自己的逻辑而不必去动Mybatis固有的逻辑。通过Mybatis拦截器我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法。所以Mybatis拦截器的使用范围是非常广泛的。

Mybatis里面的核心对象还是比较多,如下:
在这里插入图片描述
Mybatis拦截器并不是每个对象里面的方法都可以被拦截的。Mybatis拦截器只能拦截
Executor、ParameterHandler、StatementHandler、ResultSetHandler 四个对象里面的方法。

  • Executor
    Mybatis中所有的Mapper语句的执行都是通过Executor进行的。Executor是Mybatis的核心接口。从其定义的接口方法我们可以看出,对应的增删改语句是通过Executor接口的update方法进行的,查询是通过query方法进行的。Executor里面常用拦截方法如下所示。
public interface Executor {

   ...

    /**
     * 执行update/insert/delete
     */
    int update(MappedStatement ms, Object parameter) throws SQLException;

    /**
     * 执行查询,先在缓存里面查找
     */
    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

    /**
     * 执行查询
     */
    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

    /**
     * 执行查询,查询结果放在Cursor里面
     */
    <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

    ...


}
  • ParameterHandler
    ParameterHandler用来设置参数规则,当StatementHandler使用prepare()方法后,接下来就是使用它来设置参数。所以如果有对参数做自定义逻辑处理的时候,可以通过拦截ParameterHandler来实现。ParameterHandler里面可以拦截的方法解释如下:
public interface ParameterHandler {

 ...

 /**
  * 设置参数规则的时候调用 -- PreparedStatement
  */
 void setParameters(PreparedStatement ps) throws SQLException;

 ...


}
  • StatementHandler
    StatementHandler负责处理Mybatis与JDBC之间Statement的交互。
public interface StatementHandler {

    ...

    /**
     * 从连接中获取一个Statement
     */
    Statement prepare(Connection connection, Integer transactionTimeout)  throws SQLException;

    /**
     * 设置statement执行里所需的参数
     */
    void parameterize(Statement statement) throws SQLException;

    /**
     * 批量
     */
    void batch(Statement statement)  throws SQLException;

    /**
     * 更新:update/insert/delete语句
     */
    int update(Statement statement) throws SQLException;

    /**
     * 执行查询
     */
    <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException;

    <E> Cursor<E> queryCursor(Statement statement) throws SQLException;

    ...

}

一般只拦截StatementHandler里面的prepare方法。
在Mybatis里面RoutingStatementHandler是SimpleStatementHandler(对应Statement)、PreparedStatementHandler(对应PreparedStatement)、CallableStatementHandler(对应CallableStatement)的路由类,所有需要拦截StatementHandler里面的方法的时候,对RoutingStatementHandler做拦截处理就可以了,如下的写法可以过滤掉一些不必要的拦截类。

@Intercepts({
        @Signature(
                type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class}
        )
})
public class TableShardInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (invocation.getTarget() instanceof RoutingStatementHandler) {
            // TODO: 做自己的逻辑
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        // 当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身,减少目标被代理的次数
        return (target instanceof RoutingStatementHandler) ? Plugin.wrap(target, this) : target;
    }

    @Override
    public void setProperties(Properties properties) {

    }
}
  • ResultSetHandler
    ResultSetHandler用于对查询到的结果做处理。所以如果你有需求需要对返回结果做特殊处理的情况下可以去拦截ResultSetHandler的处理。ResultSetHandler里面常用拦截方法如下:
public interface ResultSetHandler {

    /**
     * 将Statement执行后产生的结果集(可能有多个结果集)映射为结果列表
     */
    <E> List<E> handleResultSets(Statement stmt) throws SQLException;
    <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

    /**
     * 处理存储过程执行后的输出参数
     */
    void handleOutputParameters(CallableStatement cs) throws SQLException;

}

1.2 创建Interceptor实现类

我们创建的拦截器必须要实现Interceptor接口,Interceptor接口的定义为

在这里插入图片描述

在MyBatis中Interceptor允许拦截的内容是:

Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)

我们创建一个拦截Executor中的query和close的方法:
在这里插入图片描述
在这里插入图片描述

1.3 MyBatis拦截器注册的三种方式

1.XML注册

xml注册是最基本的方式,是通过在Mybatis配置文件中plugins元素来进行注册的。一个plugin对应着一个拦截器,在plugin元素可以指定property子元素,在注册定义拦截器时把对应拦截器的所有property通过Interceptor的setProperties方法注入给拦截器。因此拦截器注册xml方式如下:
在这里插入图片描述

2.配置类注册

配置类注册是指通过Mybatis的配置类中声明注册拦截器,配置类注册也可以通过Properties类给Interceptor的setProperties方法注入参数。具体参考如下:
在这里插入图片描述

3.注解方式

通过@Component注解方式是最简单的方式,在不需要转递自定义参数时可以使用,方便快捷。
在这里插入图片描述

2. 插件实现原理

2.1 初始化操作

在这里插入图片描述
该方法用来解析全局配置文件中的plugins标签,然后对应的创建Interceptor对象,并且封装对应的属性信息。最后调用了Configuration对象中的方法。 configuration.addInterceptor(interceptorInstance)
在这里插入图片描述
通过这个代码我们发现我们自定义的拦截器最终是保存在了InterceptorChain这个对象中。而InterceptorChain的定义为
在这里插入图片描述

2.2 如何创建代理对象

在解析的时候创建了对应的Interceptor对象,并保存在了InterceptorChain中,那么这个拦截器是如何和对应的目标对象进行关联的呢?
首先拦截器可以拦截的对象是 Executor,ParameterHandler,ResultSetHandler,StatementHandler.那么我们来看下这四个对象在创建的时候又什么要注意的.

2.2.1 Executor

在装饰完二级缓存后会通过pluginAll来创建Executor的代理对象。
在这里插入图片描述
进入plugin方法中,然后进入到MyBatis给我们提供的Plugin工具类的实现 wrap方法中。
在这里插入图片描述
在这里插入图片描述

2.2.2 StatementHandler

 @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();
      // 注意,已经来到SQL处理的关键对象 StatementHandler >>
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // 获取一个 Statement对象
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 执行查询
      return handler.query(stmt, resultHandler);
    } finally {
      // 用完就关闭
      closeStatement(stmt);
    }
  }

在进入newStatementHandler方法


  public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    // 植入插件逻辑(返回代理对象)
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); --2.2.1 Executor 中一致
    return statementHandler;
  }

2.2.3 ParameterHandler

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在newParameterHandler的步骤我们可以发现代理对象的创建
在这里插入图片描述

2.2.4 ResultSetHandler

在上面的newResultSetHandler()方法中,也可以看到ResultSetHander的代理对象
在这里插入图片描述

2.3 执行流程

以Executor的query方法为例,当查询请求到来的时候,Executor的代理对象是如何处理拦截请求的呢?我们来看下。当请求到了executor.query方法的时候 实际是进入了 CachingExecutor 的query,这时候会被拦截
在这里插入图片描述
实际是进入了 CachingExecutor 的query,这时候会被拦截,然后会执行Plugin的invoke方法
在这里插入图片描述
然后进入interceptor.intercept 会进入我们自定义的 FirstInterceptor对象中
在这里插入图片描述

2.4 多拦截器

如果我们有多个自定义的拦截器,那么他的执行流程是怎么样的呢?比如我们创建了两个 Interceptor 都是用来拦截 Executor 的query方法,一个是用来执行逻辑A 一个是用来执行逻辑B的。

单个拦截器的执行流程
在这里插入图片描述

1、如果说对象被代理了多次,这里会继续调用下一个插件的逻辑,再走一次Plugin的invoke()方法。这里我们需要关注一下有多个插件的时候的运行顺序。
2、配置的顺序和执行的顺序是相反的。InterceptorChain的List是按照插件从上往下的顺序解析、添加的。
3、而创建代理的时候也是按照list的顺序代理。执行的时候当然是从最后代理的对象开始。
在这里插入图片描述
这个我们可以通过实际的案例来得到验证,最后来总结下Interceptor的相关对象的作用
在这里插入图片描述

3. PageHelper分析

3.1 PageHelper的应用

3.1.1添加依赖

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
   	<version>4.1.6</version>
</dependency>

或者整合springboot

<!--  pagehelper分页插件  -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.2</version>
</dependency>

3.1.2 在项目添加配置

在这里插入图片描述
在这里插入图片描述

3.1.3 分页操作

在这里插入图片描述
查看日志我们能够发现查询出来的只有5条记录
在这里插入图片描述

3.1.3 分页操作

通过MyBatis的分页插件的使用,我们发现我们仅仅是在 执行操作之前设置了一句
PageHelper.startPage(1,5);
并没有做其他操作,也就是没有改变任何其他的业务代码。这就是它的优点,那么我再来看下他的实现原理

3.2 实现原理剖析

在PageHelper中,肯定有提供Interceptor的实现类,通过源码我们可以发现是PageInterceptor,而且我们也可以看到在该方法头部添加的注解,声明了该拦截器拦截的是Executor的query方法

在这里插入图片描述
然后当我们要执行查询操作的时候,我们知道 Executor.query() 方法的执行本质上是执行 Executor的代理对象的方法。先来看下Plugin中的invoke方法
在这里插入图片描述
interceptor.intercept(new Invocation(target, method, args));方法的执行会进入到 PageInterceptor的intercept方法中.

4.应用场景分析

在这里插入图片描述