• 橘子学Mybatis02之基本对象前置了解


    一、前置知识

    1、面向对象

    首先明白一点,面向对象是啥意思。字面意思就是你面向的是对象,也就是java世界里面万物都是对象。在mybatis这个使用java开发的框架里面,也是一样的,也就是说你看到的所有东西,最后落地都是一个个对象,包括你的那些配置文件,那些xml文件都是一样的,最后都要被框架加载读取解析封装成为一个对象在mybatis的体系中发挥作用。OK,了解一下这个后面看到那些配置文件的处理也就自然了。

    2、IO流处理

    操作系统中io调用是要发起内核函数的调用的,mybatis程序是运行在用户态的,前面我们说读取配置文件转为对象,这个读取就是一次io,众所周知,用户态进入内核态是一件很"耗"的事情。所以我们尽量减少这个操作,所以你得那些配置文件尽量少读几次,那最少得读一次,好吧,那就读一次最少。
    OK,这些基本就是前置要了解的东西,东西不多,就那么个意思。

    3、关于JDBC

    我认识一个哥们,他问我要学啥东西,我说没事干看看Mybatis,这个哥们对我说:

    不就是JDBC的封装吗,有啥可学的。

    其实他说的不错,mybatis说的简单一点就是对JDBC的封装,那既然是封装,那最后一定是落脚到了JDBC上,你可以这么想,给了你一个JDBC的代码,让你封装一下,你肯定得知道JDBC有哪些东西,然后你才能去做封装。我们先来看一下JDBC有哪些组件。

    Connection conn=DriverManager.getConnection(url, user, password); 
    String sql="select * from user"; 
    //statement每次执行sql,相关数据库都要执行sql语句的编译,preparedstatement是预编 译的, 
    //preparedstatement支持批处理,处理多次请求的效率高 PreparedStatement 
    preparedStatement=conn.prepareStatement(sql);
    //调用executeQuery()方法,返回一个结果集rs,到此,数据库查询数据的工作就完成了 
    ResultSet rs=preparedStatement.executeQuery();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    上面就是jdbc的主要操作,可以看到主要就是三个类:

    • Connection 连接:和数据库建立连接的类
    • Statement:在连接之后负责执行sql的类,这个是个接口,他有多种实现,默认的那种不能预防sql糊注入,,所以用下面两种子类的多些。
      • PreparedStatement:这个是预编译执行的,效率高,大部分现在都用这个
      • CallableStatement:这个是执行存储过程的,现在应该是狗都不用了
    • ResultSet:执行sql之后返回的结果集的类
      mybatis底层其实就是对这些做了封装处理。

    二、Mybatis的主要类

    mybatis有这么几种对象:

    • 封装信息类的对象:
      我们前面说了,在面向对象的世界里面,你要把所有的东西都封装在一个对象里面,
    • 执行操作的对象:
      我们最终是要执行jdbc操作的,所以还需要一些执行操作的对象类,mybatis同样提供这类对象。

    1、封装信息的对象

    我们在编写mybatis代码的时候,有两种配置文件,一种是mybatis的核心配置,一种是你的sql的mapper文件,这两种文件都是要封装在对象里面的,因为你不能在使用的时候随时做读取解析吧,这种io操作必然不能多做,只能是一次读取解析封装完毕了,就放在对象里面了,以后随用随拿。

    1.1、Configuration

    这个对象就是用来封装mybatis的核心配置文件的,我们来看一下这个类。这个类里面有很多属性,我们先看几个核心的主要的。
    这个类就是封装的核心配置文件,也就是我们之前例子里面的那个sqlMapConfig.xml文件,里面所有你的配置都可以来这里找到,进而做解析封装进来。我们来看几个。

    // 封装了environment属性,这个是你jdbc数据源的环境配置
    protected Environment environment;
    // 这个是一级缓存是不是打开,默认是true,就是打开的
    protected boolean cacheEnabled = true;
    // 执行器,我们看到类型默认是SIMPLE,后面跟源码的时候可以看到就是这个的执行
    protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
    // MappedStatement这个其实是你mapper。xml文件的封装。下面会详细说这个
    protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
          .conflictMessageProducer((savedValue, targetValue) ->
              ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
    protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
    protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
    protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这里列举了几个主要的,我只是想说明一个问题,就是你那个核心配置类里面的属性都能解析封装到这个对象里面,这里我没列全,实际上,这个类里面有多少属性,那个配置文件就能配置多少,所以有时候官方打不开了,可以来源码这里看看具体能配置啥。
    此外这个类下面还有这么几个对象:

    // 我们看到其实他还帮你new出来很多处理器,都在这个类里面,所以这个Configuration是十分全面的。
    // 而且Configuration还帮我们创建了下面这几个核心对象,所以Configuration实际上是他们几个的工厂。
    // 这几个类由Configuration来创建。所以他很牛逼。核心类。
    public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
        parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
        return parameterHandler;
      }
    
      public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
          ResultHandler resultHandler, BoundSql boundSql) {
        ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
        resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
        return resultSetHandler;
      }
    
      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);
        return statementHandler;
      }
    
      public Executor newExecutor(Transaction transaction) {
        return newExecutor(transaction, defaultExecutorType);
      }
    
      public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Executor executor;
        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);
        }
        if (cacheEnabled) {
          executor = new CachingExecutor(executor);
        }
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    有了这个类,mybatis在启动的时候就加载了配置文件,然后读取,解析,最后把你的配置封装到这个对象里面,以后随处用的时候直接拿出来用就可以了。

    1.2、MappedStatement

    我们还有一个配置文件,也就是你写sql的那些mapper.xml的文件,这类文件也是不能经常io,只读取一次就行了。那么他被封装到了这个MappedStatement的对象里面。

    private String resource;
    // 他这里其实还持有了Configuration 这个对象,Configuration 也封装了所有的MappedStatemen,他们能互相拿到。灵活度更高。
    private Configuration configuration;
    // 这就是你写sql标签里面的那个id,什么selectById就是这个玩意,在这里封装你看他这里的设计,一个id就是一个对象,所以他其实这个对象更细分的话他是封装的xml里面的一个增删改查的标签片段,但是我们又说了selectById这个只是方法名,每个mapper里面都可能有,所以他光靠这个不能确定唯一,他需要namespace结合一起来唯一确定,所以这个对象里面的id是由namespace+mapper中的id来组合确定的。
    private String id;
    // 每次查询的个数,批次大小,
    private Integer fetchSize;
    // 超时时间
    private Integer timeout;
    // 你使用的jdbc的Statement类型
    private StatementType statementType;
    private ResultSetType resultSetType;
    private SqlSource sqlSource;
    private Cache cache;
    private ParameterMap parameterMap;
    private List<ResultMap> resultMaps;
    private boolean flushCacheRequired;
    private boolean useCache;
    private boolean resultOrdered;
    private SqlCommandType sqlCommandType;
    private KeyGenerator keyGenerator;
    private String[] keyProperties;
    private String[] keyColumns;
    private boolean hasNestedResultMaps;
    private String databaseId;
    private Log statementLog;
    private LanguageDriver lang;
    private String[] resultSets;
    // 这个类的构造方法,是个建造者模式
    public static class Builder {
        private MappedStatement mappedStatement = new MappedStatement();
    
        public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
          mappedStatement.configuration = configuration;
          mappedStatement.id = id;
          mappedStatement.sqlSource = sqlSource;
          // 我们看到了StatementType 属性默认是PreparedStatement这个预编译的,可见他也支持用这个,当然了你可以在配置文件里面修改,类似下面的例子,所以他默认给你的是这个预编译的statement
          mappedStatement.statementType = StatementType.PREPARED;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    在这里插入图片描述

    1.3、一个规则

    我们在Configuration里面看到了这么一类属性

    // MappedStatement这个其实是你mapper。xml文件的封装。下面会详细说这个
    protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
          .conflictMessageProducer((savedValue, targetValue) ->
              ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
    
    • 1
    • 2
    • 3
    • 4

    我们看到了他封装了一个map,map里面是key-MappedStatement的一个key-value这个关系,那么他啥意思呢,他是在你封装了mapper.xml的MappedStatement的对象作为一个个的map,因为可能有多个xml所以最后是多个,那么这个多个他用key去做的区分,这个key要唯一区别的话第一个就是你写的那个id在xml里面要唯一,但是话又说回来了,我们可不止一个userMapper.xml,还可能有studentMapper.xml,那个id只是在自己的xml里面唯一的,但是你要封装到这个全局的map里面这个id就不能保证唯一key了,所以他实际上是依赖的userMapper.xml的两个部分来区分的,
    也就是:

    <mapper namespace="com.yx.dao.IUserDao">
    
      <select id="findAll" resultType="com.yx.domain.User" statementType="CALLABLE">
              SELECT * FROM `user`
      select>
    其实就是namespace和id合起来做唯一区分,至于你是用逗号隔开,还是空格啥的无所谓了,实际上mybatis是用.来隔开的。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    基于以上我们可以得出这么一个结果,就是你的核心配置文件疯转成为Configuration,里面的每一个配置都在对象中有一个属性能对应上。
    而你mapper.xml是封装在MappedStatement这个对象里面的,但是他封装的维度不是一整个文件,而是文件里面的一个个sql标签,区分这些sql标签的方式就是namesapce.id。具体可以参考下面这个图示。
    在这里插入图片描述

    2、处理行为的对象

    上面我们已经解析了配置文件,把对应的信息封装到了对象里面,以后想用的时候随时拿来用就行了。我们这里先看一下我从网上找的一个架构图。而我们上面看到这类操作类的对象其实也是在Configuration里面创建的,Configuration是他们的工厂。这类对象就要负责对最终的数据库做操作,实现数据的操作。
    在这里插入图片描述
    大家都能看到,我们最终暴露给客户端调用的就是sqlSession这个类,他下面执行是调用的excutor这个类,看名字也知道是执行器的意思,我盲猜这里就是真正的操作的地方,我们去看一下这个类。

    2.1、Executor执行器

    他是个接口,里面定义了方法的模板实现,他是mybatis中处理功能的核心。而代码中设计的操作只有query和update,这里需要说一下就是在代码中的update其实对应到数据库操作其实是增删改,而查就是query,其他的缓存操作,事务操作他也是支持的。

    他的实现类包括:BatchExecutor(批量操作,在jdbc中每一次执行sql都要做一次连接都要创建statement,所以批量执行其实是很有必要支持的,所以其实也支持batch操作。),ReuseExecutor(复用statement对象的时候,sql完全不变,包括参数之类的,就可以复用statement来重复执行sql),SimpleExecutor(最基本的,最常用的,也是mybatis默认的。),
    我们看到这些方法也都是对应的你对数据库做的操作方法。其主要方法也就是这么几个

    • sql的生成和执行
    • 缓存的处理
    • 事务的管理
      这么多内容都在这里做了定义,我们这里先只看sql的执行,我们在上面的图里面可以看到,他是把第一个功能的sql的生成和执行都交给了StatementHandler这个类,真正做到了职责单一,把每一个功能都封装到不同的类里面,各司其职,坚决不混淆。这种设计是很好的。然后每个职责的类都定义为接口,去做不同的实现,利用多态做到扩展性。比如mybatis就支持批量的操作,默认的操作,简单的操作这都是不同的Executor实现。这里有一个原则就是在java开发中所有的操作类型的类,就是这个类的功能是用来操作数据的,其实都应该考虑设计为接口。即使你不知道哪些实现,为了你后续的扩展也应该考虑设计为接口。mybatis中大量使用这个原则,比如ssqlSession。
      Eexcutor是真正开始操作的地方,但是他是上层,他会把任务交给下面的各个类来完成。其中我们知道Statement是操作sql的,而StatementHandler是封装了jdbc的Statement,而Executor就是把操作sql交给了StatementHandler。

    2.2、StatementHandler的sql处理

    实际上StatementHandler也就是完成了sql的执行,但是事务,缓存其实还在其他地方。他完成的是最基本的。
    这里就是封装了jdbc的Statement,而jdbc中的sql就是Statement操作的,所以这里其实就是执行sql操作了。进行jdbc操作。
    在这里插入图片描述
    他也是一个接口,这种操作类定义成为接口做多态扩展的实现在mybatis里面层出不穷。我们看下他的实现类。
    其中base是一个适配器,不是最终真正实现的,上面的Executor其实也是这样。
    在这里插入图片描述
    在这里插入图片描述
    所以我们到这里先局部总结一下:

    我们建立会话开始处理最外层的类是SqlSession:但是他啥也不干,就是做了个封装。
    SqlSession把执行的具体操作交给了Excutor类,这个类负责大体三个功能,执行sql,管理缓存,操作事务。
    其中他把执行sql这个操作交给了StatementHandler(看名字也知道,因为最后是和jdbc打交道,其实就是Statement),其他两个管理缓存,操作事务他不负责。
    
    • 1
    • 2
    • 3

    2.3、ParameterHandler的处理

    上面是把sql执行都做了,但是还有个问题就是参数,我们在mybatis写的sql来看都是那种#{}这种的,StatementHandler最后执行的时候人家从BoundSQL里面拿到的sql必须就是要一个最终的sql,所以还要做参数的替换,这种你自己也可以实现一下,就是个正则表达式替换,把我们mybatis的参数替换为jdbc的参数,最终sql封装在BoundSQL交给Statement。
    然后mybatis把参数处理的工作交给了ParameterHandler下面的实现。你其实可以看到,他的这种职责划分的是很细致的。

    2.4、TypeHandler的处理

    最后执行的时候,以及执行完了做返回的时候,你执行的时候,要传参,最后执行完了返回的时候有结果,
    这里面涉及到一个mybatis的java类型和数据库的mysql类型的转换映射,举个例子,你mybatis里面写的时候是string类型,到了数据库可能就是varchar类型。这种需要做转换,不然类型不对应,这个转换工作就是TypeHandler来做的,因为这个转换可能发生在执行sql的时候的参数转换以及查询结果的结果类型转换,所以其一定是和StatementHandler和ParameterHandler有关系的。
    这个转换是双向的,你查的时候要把java的类型转去数据库,也要查回来把数据库类型转为java。

    2.5、ResultSetHandler的处理

    上面有了sql了,类型转换也完了,最后就是执行sql,那这个执行结果查出来之后这个结果集的封装就是在这里做的。返回给程序,他是和jdbc重的ResultSet差不多一个意思。

    3、总结一下

    我们一直说mybatis是jdbc的封装,jdbc是java执行数据库sql的工具。那我们这里面其实就是涉及到了java执行以及jdbc这两个内容。
    其中jdbc包括:

    • sql
    • statement
    • ResultSet
    这几个东西,那我们换位思考,我们设计mybatis的时候就是封装这几个东西就行了,所以他拆分了几个功能,
    每个功能都负责对应的事情。我们从底向上开始解析一下:
    其中StatementHandler负责封装statement,最后和sql打交道执行。
    其中ParameterHandler负责封装对你xml里面参数的转换处理。最后传进去sql文本。
    其中ResultSetHandler负责封装查询结果集的结果。最后返回给客户端。
    其中TypeHandler负责对传参时候以及查询结果的数据库和java之间的类型转换。
    最后他们统一在Excutor这个类里面做调用,其中Excutor不仅仅是上面那些执行sql的操作,人家还管着缓存和事务的功能。Excutor再封装给sqlSession最后做统一调用。默认的是DefaultSqlSession。
    最终他们这些功能处理的类,统一封装成门面也就是sqlSession暴露给程序员客户端做调用。至此为止,这就是mybatis执行类的所有内容。示意图如下。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述

    三、思辨

    1、我们看到了这些结构,他把文件解析出来,拆成了一个个的对象,而且把对象拆的很细分,各处理各的,这是一种单依职责的体现,一个类只负责一个功能,在以后修改的时候我们只需要改这个类就行了,对其他类影响的也不多,实际开发可以基于这个实现一些你的代码。不要过度冗余一个类。

    再补充一个例子:假如我们来设计mybatis的类结构,我们在封装MappedStatement的时候会怎么做呢?
    我的第一反应就是,我把mapper.xml这个文件解析了,然后封装成为一个个的小对象,MappedStatement里面持有一个这个小对象的集合。这个小对象就是包含了这些sql标签,我可能会直接把标签上面的属性和sql文本都封装到一起,一个大类。
    但是mybatis设计的时候他把文本单独拆出去了,又组合了一部分的属性,参数,这样在后面解析sql的时候对象的职责就隔离的很开,改动的时候不会改了这里,影响到哪里,做到了单依职责的解耦。这一点需要我们注意。

    2、关于执行

    @Test
    public void test1() throws IOException {
        IUserDao userDao = sqlSession.getMapper(IUserDao.class);
        List<User> userList = userDao.findAll();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    我们上面分析了一大堆,最后知道了过程是sqlSession调用excutor以及后面的一堆handler处理的整个执行流程。那么问题来了,我实际写代码的时候是上面那两行,我调用的是我userDAO里面的findAll自己定义的方法,我这个方法是怎么和你的那些东西扯上关系的呢,而且你有没有发现一件事情。我的dao一直就是一个接口,也没有实现类,接口最后是怎么实现的呢?此时需要引出一个东西,叫做代理模式,后面我写一下静态代理和动态代理的东西。

  • 相关阅读:
    Nginx网站服务
    CSS 3之背景属性
    SpringBoot是什么?如何学习SpringBoot?
    阅读记录【PMLR2023】The Aggregation–Heterogeneity Trade-off in Federated Learning
    ES6(ECMASript 6 新特性---Set,Map,class类)
    货物摆放(蓝桥杯)
    STL教程6-deque、stack、queue、list容器
    模拟对抗之红队免杀开发实践
    RESTful风格介绍
    分类预测 | Matlab实现基于PSO-SDAE粒子群优化算法优化堆叠去噪自编码器的数据分类预测
  • 原文地址:https://blog.csdn.net/liuwenqiang1314/article/details/127118975