美文网首页
MyBatis源码学习

MyBatis源码学习

作者: 小毛1221 | 来源:发表于2019-11-05 22:13 被阅读0次

    mybatis 不会直接和数据库进行打交道,mybatis 其实是对 jdbc api 的进一步封装,最终和数据库打交道的仍然是 jdbc 。

    1. MyBatis基本构成

    • SqlSessionFactoryBuilder(构造器):它会根据配置信息或者代码来生成SqlSessionFactory(工厂接口);
      • 生命周期:它的作用就是一个构造器,一旦我们构建了SqlSessionFactory,SqlSessionFactoryBuilder的作用就已经完结。所以它的生命周期仅存在于方法局部。
    • SqlSessionFactory:依靠工厂来生成SqlSession(会话);
      • 生命周期:SqlSessionFactory的作用是创建SqlSession,而SqlSession就是一个会话,相当于JDBC中的Connection对象。每次应用程序需要访问数据库,我们就通过SqlSessionFactory创建SqlSession,所以SqlSessionFactory应该在MyBatis应用的整个生命周期中。而如果我们多次创建同一个数据库的SqlSessionFactory,则每次创建SqlSessionFactory会打开更多的数据库连接(Connection)资源,那么连接资源就很快会被耗尽。因此SqlSessionFactory是一个全局单例,对应一个数据库连接池。
    • SqlSession:是一个既可以发送SQL去执行并返回结果,也可以获取Mapper的接口。
      • 生命周期:SqlSession相当于一个JDBC的Connection对象,在一次请求事务会话后,我们会将其关闭。
    • SQL Mapper:它是由一个Java接口和XML文件(或注解)构成的,需要给出对应的SQL和映射规则。它负责发送SQL去执行,并返回结果;
      • 生命周期:Mapper的作用是发送SQL,然后返回我们需要的结果,因此它应该在一个SqlSession事务方法之内,是一个方法级别的东西,声明周期与SqlSession相同。

    2. SqlSessionFactoryBuilder

    通过XMLConfigBuilder解析配置的XML文件,读出配置参数并存入Configuration类中。(MyBatis中几乎所有的配置都是存在这里的)
    使用了建造者模式,主要过程就是解析xml配置文件,new SqlSessionFactory

    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
      try {
        /**
         * 1. 根据inputStream等信息创建XMLConfigBuilder对象;
         * 2. XMLConfigBuilder会将XML配置文件的信息转换为Document对象;
         */
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        /**
         * 3. parser.parse()会处理每个node并返回Configuration对象,解析此Node节点的子Node,获取相关属性:properties, settings, typeAliases,typeHandlers,
         *  objectFactory, objectWrapperFactory, plugins, environments,databaseIdProvider, mappers
         * 4. 如解析typeHandlers的过程就是将TypeHandler.class注册到一个hashmap中
         * 5. 最后调用build方法 new DefaultSqlSessionFactory(config)
         */
        return build(parser.parse());
      } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", e);
      } finally {
        ErrorContext.instance().reset();
        try {
          inputStream.close();
        } catch (IOException e) {
          // Intentionally ignore. Prefer previous error.
        }
      }
    }
    
    public SqlSessionFactory build(Configuration config) {
      return new DefaultSqlSessionFactory(config);
    }
    

    3. SqlSessionFactory

    使用Configuration对象去创建SqlSessionFactory(默认的实现为DefaultSqlSessionFactory)。
    主要涉及二级缓存的Executor,直接new DefaultSqlSession

    public SqlSession openSession(ExecutorType execType) {
      return openSessionFromDataSource(execType, null, false);
    }
    
    private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
      Transaction tx = null;
      try {
        final Environment environment = configuration.getEnvironment();
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        /**
         * new Transaction, transactionFactory的实现类有JdbcTransactionFactory, ManagedTransactionFactory,SpringManagedTransactionFactory
         * Spring与MyBatis整合后使用SpringManagedTransactionFactory,将事务委托给Spring
         */
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        /**
         * 创建Executor(事务包含在其中)
         * 如果开启了二级缓存,会创建CachingExecutor,一个装饰器
         * 先去SqlSessionFactory级别的二级缓存中查,如果查到就使用,查不到则调用原有Executor的查询方法
         */
        final Executor executor = configuration.newExecutor(tx, execType);
        // 直接new DefaultSqlSession
        return new DefaultSqlSession(configuration, executor, autoCommit);
      } catch (Exception e) {
        closeTransaction(tx); // may have fetched a connection so lets call close()
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
      } finally {
        //最后清空错误上下文
        ErrorContext.instance().reset();
      }
    }
    

    4. SqlSession

    流程:

    • SqlSession -> Executor -> StatementHandler(调用prepare方法进行预编译) -> ParameterHandler(设置预编译sql的参数) -> StatementHandler调用PreparedStatement执行sql -> ResultHandler封装结果
    • Mapper映射是通过动态代理实现的,在MapperProxy中会根据SQL的类型(insert、update、delete、select)调用SqlSession的对应方法;
    • SqlSession中的insert、update、delete、select方法实际上是调用Executor的对应方法;
      • SqlSession下的四大对象
      • Executor代表执行器,由它来调度StatementHandler、ParameterHandler、ResultHandler等来执行对应的SQL;
      • StatementHandler的作用是使用数据库的Statement(PreparedStatement(预编译的,效率高,可以使用占位符代替参数从而多次执行))来执行sql操作;
      • ParameterHandler用于SQL对参数的处理,使用TypeHandler向PreparedStatement中设置参数;
      • ResultHandler是进行最后数据集(ResultSet)的封装返回的处理;

    4.1 select

    这里以DefaultSqlSession为例。

    DefaultSqlSession中有多个select方法,如selectOne, selectMap, selectList等,但都是以selectList为基础,如selectOne是调用selectList,然后list.get(0)得到结果。selectMap也是先selectList,然后遍历得到的list,转换为Map。

    所以我们来看一下selectList

    @Override
    public <E> List<E> selectList(String statement, Object parameter) {
      return this.selectList(statement, parameter, RowBounds.DEFAULT);
    }
    
    @Override
    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
      try {
        /**
         * 根据statement获取MappedStatement
         * statement其实就是MappedStatement的id,为被调用Mapper的类名,如com.example.shiro.mapper.UserMapper.selectByPrimaryKey,见下图
         */
        MappedStatement ms = configuration.getMappedStatement(statement);
        /**
         * 调用之前创建的Executor query方法,没有开启二级缓存则使用BaseExecutor
         * 注意这里传入的ResultHandler为null,
         * 在后续query过程中,如果ResultHandler为null则先尝试从缓存中取,
         * 如果ResultHandler不为null则不会尝试从缓存中取
         */
        return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
      } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
      } finally {
        ErrorContext.instance().reset();
      }
    }
    
    image.png

    再来看一下BaseExecutor#query()方法

    @Override
     public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
       /**
        * BoundSql中包含需要动态生成的sql语句,以及对应的参数
        */
       BoundSql boundSql = ms.getBoundSql(parameter);
       /**
        * 根据statementId, params, rowBounds来构建一个key值,MyBatis认为这几个参数能够代表同一个sql
        */
       CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
       return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
    
    
    
     @SuppressWarnings("unchecked")
     @Override
     public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
       ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
       if (closed) {
         throw new ExecutorException("Executor was closed.");
       }
       // 当queryStack == 0时才清空缓存
       if (queryStack == 0 && ms.isFlushCacheRequired()) {
         clearLocalCache();
       }
       List<E> list;
       try {
         // 保证在执行过程中不会清空缓存
         queryStack++;
        /**
         * localCache一级缓存,内部为一个HashMap,线程不安全的
         * resultHandler DefaultSqlSession中传入的ResultHandler为null
         * 注意:这里从cache中获取到的结果强转为list,queryFromDatabase会先传入一个占位符,如果此时有另一个线程进来,再强转则会抛异常,相当于做了多线程操作的处理
         */
         list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
         if (list != null) {
           handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
         } else {
           // 缓存中没有则从数据库中查询
           list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
         }
       } finally {
         queryStack--;
       }
       if (queryStack == 0) {
         //延迟加载队列中所有元素
         for (DeferredLoad deferredLoad : deferredLoads) {
           deferredLoad.load();
         }
         // issue #601
         //清空延迟加载队列
         deferredLoads.clear();
         if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
           // issue #482
           clearLocalCache();
         }
       }
       return list;
     }
    
    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
      List<E> list;
      /**
       * 在缓存中放入一个占位符 enum类型
       * 当第一个线程正常向数据库中查询时,第二个线程也执行了相同的查询
       * 在BaseExecutor#query方法中(List<E>) localCache.getObject(key),此时报类型转换异常,防止多线程操作缓存
       */
      localCache.putObject(key, EXECUTION_PLACEHOLDER);
      try {
        /**
         * 调用StatementHandler#query方法
         * 如实现类PreparedStatementHandler,就是调用PreparedStatement#excute方法
         */
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
      } finally {
        // 删除占位符
        localCache.removeObject(key);
      }
      //将查询结果放入缓存
      localCache.putObject(key, list);
      if (ms.getStatementType() == StatementType.CALLABLE) {
        localOutputParameterCache.putObject(key, parameter);
      }
      // 直接返回缓存结果的引用
      return list;
    }
    

    可以看到,在queryFromDatabase中直接返回了缓存结果的引用,这样就会出现脏读,如下代码:
    在一次查询后,修改查询结果,下一次查询时直接从缓存中查询,结果会发现第二次的查询结果也被修改了

    
    @Transactional(rollbackFor = Exception.class)
    public User getUser(Long id) {
        User user = userMapper.selectByPrimaryKey(id);
        log.info("User: {}", user.getUsername());
        user.setUsername("test-mybatis-cache");
        User user2 = userMapper.selectByPrimaryKey(id);
        log.info("User2: {}", user2.getUsername());
    
    
        return user;
    }
    

    缓存中返回的引用,在一次事务中MyBatis不会清空缓存,所以修改引用后,下次查询得到的结果会有问题

    image.png
    去掉@Transactional后变得正常了
    image.png
    其实SqlSession 一级缓存的查询工作流程为:
    1. 对于某个查询,根据statementId, params, rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果;
    2. 判断从Cache中根据特定的key值取的数据数据是否为空,即是否命中;
    3. 如果命中,则直接将缓存结果返回;
    4. 如果没命中:
      a. 去数据库中查询数据,得到查询结果;
      b. 将key和查询到的结果分别作为key,value对存储到Cache中;
      c. 将查询结果返回;

    MyBatis认为,对于两次查询,如果以下条件都完全一样,那么就认为它们是完全相同的两次查询:

    1. 传入的 statementId
    2. 查询时要求的结果集中的结果范围 (结果的范围通过rowBounds.offset和rowBounds.limit表示);
    3. 这次查询所产生的最终要传递给JDBC java.sql.Preparedstatement的Sql语句字符串(boundSql.getSql() )
    4. 传递给java.sql.Statement要设置的参数值

    现在分别解释上述四个条件:

    1. 传入的statementId,对于MyBatis而言,你要使用它,必须需要一个statementId,它代表着你将执行什么样的Sql;
    2. MyBatis自身提供的分页功能是通过RowBounds来实现的,它通过rowBounds.offset和rowBounds.limit来过滤查询出来的结果集,这种分页功能是基于查询结果的再过滤,而不是进行数据库的物理分页;

    由于MyBatis底层还是依赖于JDBC实现的,那么,对于两次完全一模一样的查询,MyBatis要保证对于底层JDBC而言,也是完全一致的查询才行。而对于JDBC而言,两次查询,只要传入给JDBC的SQL语句完全一致,传入的参数也完全一致,就认为是两次查询是完全一致的。
    上述的第3个条件正是要求保证传递给JDBC的SQL语句完全一致;第4条则是保证传递给JDBC的参数也完全一致;
    即3、4两条MyBatis最本质的要求就是:
    调用JDBC的时候,传入的SQL语句要完全相同,传递给JDBC的参数值也要完全相同。

    4.2 insert & delete

    // DefaultSqlSession#insert
    
    
    @Override
    public int insert(String statement) {
      return insert(statement, null);
    }
    
    @Override
    public int insert(String statement, Object parameter) {
      return update(statement, parameter);
    }
    
    
    // DefaultSqlSession#delete
    @Override
    public int delete(String statement) {
      return update(statement, null);
    }
    
    @Override
    public int delete(String statement, Object parameter) {
      return update(statement, parameter);
    }
    

    可以看到insert和delete方法其实就是调用了update(会清空一级缓存),下面来看一下update方法

    4.3 update

    @Override
    public int update(String statement) {
      return update(statement, null);
    }
    
    @Override
    public int update(String statement, Object parameter) {
      try {
        /**
         * dirty置为true,在commit和rollback方法中会判断isCommitOrRollbackRequired(),
         * 如果dirty为true则表明需要commit,会调用transaction.commit();
         */
        dirty = true;
        MappedStatement ms = configuration.getMappedStatement(statement);
        // 直接调用Executor#update
        return executor.update(ms, wrapCollection(parameter));
      } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
      } finally {
        ErrorContext.instance().reset();
      }
    }
    

    看一下BaseExecutor#update

    @Override
    public int update(MappedStatement ms, Object parameter) throws SQLException {
      ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
      if (closed) {
        throw new ExecutorException("Executor was closed.");
      }
      /**
       * 先清空本地缓存
       * localCache.clear();
       */
      clearLocalCache();
      // doUpdate,一个模板方法,交给子类实现
      return doUpdate(ms, parameter);
    }
    

    SimpleExecutor#doUpdate

    @Override
    public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
      Statement stmt = null;
      try {
        Configuration configuration = ms.getConfiguration();
        /**
         * 创建StatementHandler
         * StatementHandler负责处理Mybatis与JDBC之间Statement的交互,如PreparedStatement
         */
        StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
        /**
         * 1. 创建JDBC连接Connection(使用连接池)
         * 2. 调用StatementHandler#prepare方法,通过Connection创建PreparedStatement
         * 3. 调用StatementHandler#parameterize方法,向PreparedStatement中设置sql参数,TypeHandler就是在这里生效的 
         */
        stmt = prepareStatement(handler, ms.getStatementLog());
        /**
         * 直接调用PreparedStatement#execute方法,执行sql并返回受影响行数
         */
        return handler.update(stmt);
      } finally {
        /**
         * 关闭statement
         */
        closeStatement(stmt);
      }
    }
    

    总体来说,BaseExecutor#update方法比较简单,无非就是先清空本地一级缓存,再调用PreparedStatement执行sql。

    4.4 commit & rollback

    @Override
    public void commit(boolean force) {
      try {
        /**
         * isCommitOrRollbackRequired(),根据dirty、autoCommit、force判断是否需要提交或回滚
         * 先清空缓存,再transaction.commit()
         */
        executor.commit(isCommitOrRollbackRequired(force));
        /**
         * dirty置为false,下次无需提交或回滚
         */
        dirty = false;
      } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
      } finally {
        ErrorContext.instance().reset();
      }
    }
    
    @Override
    public void rollback(boolean force) {
      try {
        /**
         * isCommitOrRollbackRequired(),根据dirty、autoCommit、force判断是否需要提交或回滚
         * 先清空缓存,再transaction.rollback()
         */
        executor.rollback(isCommitOrRollbackRequired(force));
        /**
         * dirty置为false,下次无需提交或回滚
         */
        dirty = false;
      } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error rolling back transaction.  Cause: " + e, e);
      } finally {
        ErrorContext.instance().reset();
      }
    }
    
    • 当使用MyBatis-Spring时,在org.mybatis.spring.SqlSessionTemplate中会调用SqlSession#commit,如果添加了@Transactional注解,在commit后sql才会生效,如果有没有添加注解commit前sql就已经生效了。这里无论是否添加了@Transactional注解都需要执行SqlSession#commit主要是考虑到有的数据库必须在close前调用commit或rollback;
    • 当使用MyBatis-Spring时,@Transactional注解会使用Spring的事务,则不会调用SqlSession#rollback方法;

    5. MyBatis-Spring

    文档:http://mybatis.org/spring/zh/getting-started.html

    • 一个使用 MyBatis-Spring 的其中一个主要原因是它允许 MyBatis 参与到 Spring 的事务管理中。而不是给 MyBatis 创建一个新的专用事务管理器,MyBatis-Spring 借助了 Spring 中的 DataSourceTransactionManager 来实现事务管理。
    • 一旦配置好了 Spring 的事务管理器,你就可以在 Spring 中按你平时的方式来配置事务。并且支持 @Transactional 注解和 AOP 风格的配置。在事务处理期间,一个单独的 SqlSession 对象将会被创建和使用。当事务完成时,这个 session 会以合适的方式提交或回滚。
    • 不能在 Spring 管理的 SqlSession 上调用 SqlSession.commit(),SqlSession.rollback() 或 SqlSession.close() 方法。如果这样做了,就会抛出 UnsupportedOperationException 异常。在使用注入的映射器时,这些方法也不会暴露出来。
    • DefaultSqlSession中的一级缓存就是一个HashMap,它不是线程安全的,MyBatis-Spring中SqlSessionTemplate是线程安全的,它将SqlSession存储在org.springframework.transaction.support.TransactionSynchronizationManager中,TransactionSynchronizationManager中使用ThreadLocal变量保存SqlSession。每个线程过来都是一个独立的SqlSession,所以能够保证线程安全。https://my.oschina.net/u/3145456/blog/1841572
    private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");
    

    6. SQL Mapper

    • 它是由一个Java接口和XML文件(或注解)构成的,需要给出对应的SQL和映射规则。它负责发送SQL去执行,并返回结果;
    • 在Spring启动时(getBean),会初始化所有的Mapper类,并生成对应的代理类MapperProxy。
    • 当执行Mapper中的方法时(如userMapper.insert(user)),会调用Mapper的代理类MapperProxy(所以Mapper需要是接口),MapperProxy会调用SqlSessionTemplate的对应方法,如下:
    @Override
    public int insert(String statement, Object parameter) {
      return this.sqlSessionProxy.insert(statement, parameter);
    }
    
    • 而这里的sqlSessionProxy是在SqlSessionTemplate的构造函数中创建的动态代理类(主要处理了SqlSession的线程安全问题,最终还是直接调用DefaultSqlSession的对应方法)
    public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
        PersistenceExceptionTranslator exceptionTranslator) {
    
      notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
      notNull(executorType, "Property 'executorType' is required");
    
      this.sqlSessionFactory = sqlSessionFactory;
      this.executorType = executorType;
      this.exceptionTranslator = exceptionTranslator;
      // 创建SqlSession的动态代理类,需要看下SqlSessionInterceptor
      this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
          new Class[] { SqlSession.class }, new SqlSessionInterceptor());
    }
    
    private class SqlSessionInterceptor implements InvocationHandler {
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        /**
         * 1. 尝试去TransactionSynchronizationManager的Threadlocal中寻找SqlSession(线程安全的)
         *  1.1 如果能够获取到,则直接返回(此时需要计数器加一,用于记录SqlSession被获取了多少次)
         *  1.2 如果获取不到
         *    1.2.1 创建新的SqlSession
         *    1.2.2 如果当前存在事务,则向TransactionSynchronizationManager的Threadlocal中注册新创建的SqlSession
         *    1.2.3 如果没有事务,则不会向TransactionSynchronizationManager注册,所以每次都是新的SQLSession
         */
        SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
            SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
        try {
          // 可以看到invoke函数的参数中proxy是没有被用到的,这里直接传入的是动态获取的SqlSession
          Object result = method.invoke(sqlSession, args);
          if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
            // force commit even on non-dirty sessions because some databases require
            // a commit/rollback before calling close()
            sqlSession.commit(true);
          }
          return result;
        } catch (Throwable t) {
          Throwable unwrapped = unwrapThrowable(t);
          if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
            // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
            closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
            sqlSession = null;
            Throwable translated = SqlSessionTemplate.this.exceptionTranslator
                .translateExceptionIfPossible((PersistenceException) unwrapped);
            if (translated != null) {
              unwrapped = translated;
            }
          }
          throw unwrapped;
        } finally {
          if (sqlSession != null) {
            /**
             * 关闭SqlSession
             * 如果TransactionSynchronizationManager中存在SqlSession,减少一个计数(holder.released()),并不直接close SqlSession
             * 如果TransactionSynchronizationManager不存在,直接调用SqlSession#close方法
             */
            closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          }
        }
      }
    }
    
    • 关闭SqlSession:
      • 如果不存在事务,每次执行一个Mapper的方法时,都会创建一个新的SqlSession,执行完毕后关闭;
      • 如果存在事务,在事务的过程中,使用的是相同的SqlSession,事务结束后,会关闭SqlSession;
      • 也就是说,在事务中才会用到SqlSession的一级缓存,而无事务时,没法触发一级缓存。

    7. MyBatis两级缓存

    1. 默认开启一级缓存,PerpetualCache对象就是使用HashMap来做的(一级缓存只是相对于SqlSession而言
    2. 在参数和SQL完全一样的情况下,我们使用同一个SqlSession对象调用同一个Mapper的方法,往往只执行一次SQL,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession都只会取出当前缓存的数据,而不会再次发送SQl到数据库。
    3. 如果使用不同的SqlSession对象,因为不同的SqlSession都是相互隔离的,所以用相同的Mapper、参数和方法,会发送多次SQL到数据库。
    4. 二级缓存是在Mapper级别的,默认是不开启的,且要求返回的POJO必须是可序列化的,即实现Serializable接口。实现Serializable接口主要是因为缓存不一定是在内存中,也可能在磁盘中,所以需要进行序列化和反序列化。(开启二级缓存后默认insert、update、delete会刷新缓存,缓存使用LRU或FIFO等最近最少使用算法来回收)
    5. 二级缓存是SqlSessionFactory层面的,生命周期与SqlSessionFactory、Configuration对象相同
    6. 默认系统缓存是MyBatis所在服务器的本地缓存,如果想使用redis等缓存服务器,MyBatis也支持自定义缓存,需要实现org.apache.ibatis.cache.Cache。

    一级缓存的具体实现已经在上面阐述过了,所以这里只讨论下二级缓存。

    二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源。

    7.1 select

    CachingExecutor是一个装饰器,丰富了如SimpleExecutor的功能,提供了二级缓存的支持。
    CachingExecutor#query

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
        throws SQLException {
      /**
       * 1. 从MappedStatement中获取cache
       *  1.1 Spring boot中想要开启了二级缓存需要在Mapper上添加@CacheNamespace注解,如果添加了该注解,则这里可以获取到cache
       *  1.2 MappedStatement是在启动时,注册到org.apache.ibatis.session.Configuration中的
       *    1.2.1 每个Mapper的每个方法都会生成一个MappedStatement,且Mapper中保存了一个cache对象
       *      a. 如果没有开启二级缓存,cache对象为空
       *      b. 如果开启了二级缓存,则每个Mapper的不同方法共享同一个cache对象,即mybatis的二级缓存是Mapper级别的
       */
      Cache cache = ms.getCache();
      if (cache != null) {
        /**
         * 是否需要刷新缓存,默认情况下,select不需要刷新缓存
         */
        flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) {
          ensureNoOutParams(ms, parameterObject, boundSql);
          /**
           * tcm.getObject(cache, key)实际上就是去参数中的cache获取缓存
           */
          List<E> list = (List<E>) tcm.getObject(cache, key);
          if (list == null) {
            /**
             * 如果没有查询到,则会去被代理的Executor(如SimpleExecutor)查询
             * delegate.query也会去查询一级缓存
             */
            list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
            /**
             * 该方法并没有直接将查询的结果对象存储到其封装的二级缓存Cache对象中,
             * 而是暂时保存到entriesToAddOnCommit集合中,
             * 在事务提交时(CachingExecutor#commit)才会将这些结果从entriesToAddOnCommit集合中添加到二级缓存中
             */
            tcm.putObject(cache, key, list); // issue #578 and #116
          }
          return list;
        }
      }
      /**
       * 未开启二级缓存,则直接调用原Executor
       */
      return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
    

    下图可以看到,开启了二级缓存后,每个Mapper的不同方法共享同一个cache对象,不同Mapper的cache对象不同


    image.png
    image.png

    7.2 insert, delete, update

    CachingExecutor#update
    当SqlSession执行,insert, delete, update时,如果开启了二级缓存会调用CachingExecutor#update方法

    @Override
    public int update(MappedStatement ms, Object parameterObject) throws SQLException {
      //是否需要刷新缓存,insert,delete,update要刷新缓存
      flushCacheIfRequired(ms);
      // 直接调用原Executor
      return delegate.update(ms, parameterObject);
    }
    

    开启二级缓存时,SqlSession的声明周期与之前相同:

    • 如果不存在事务,每次执行一个Mapper的方法时,都会创建一个新的SqlSession,执行完毕后关闭;
    • 如果存在事务,在事务的过程中,使用的是相同的SqlSession,事务结束后,会关闭SqlSession;

    7.3 二级缓存的缺点

    1. 容易出现脏数据:
      a. 由于二级缓存是Mapper级别的,如UserMapper中出现了查询Role表的SQL,因为RoleMapper与UserMapper的二级缓存不同,所以使用RoleMapper更新Role表并不会刷新UserMapper中查询Role表的SQL;
      b. 同理,当在Mapper中出现关联查询时,其他Mapper修改了关联的数据表,则一定会出现脏数据;
    2. 缓存粒度只到Mapper,无法获取更细粒度的缓存;
    3. 分布式场景下,必然会出现脏数据;
      a. 一级缓存如果为开启事务,则每一个sql对应一个SqlSession对应一个一级缓存,所以不会出现脏数据;如果开启了事务,则在两次查询的间隙有他人修改,可能会出现脏数据(未强制加锁);
    4. 所以说使用二级缓存,还不如自己在业务层做一次缓存;

    8. MyBatis延迟加载

    当真正使用到这个数据时才会发送sql语句。(级联时,默认将关联的属性都查询出来,如果开启了延迟加载,则使用到关联属性时才会查找)
    实现原理:使用动态代理,会生成一个动态代理对象,里面保存着相关的SQL和参数,一旦我们使用这个代理对象的方法,它会进入到动态代理对象的代理方法里,方法里面会通过发送sql和参数,就可以把对应的结果从数据库中查找回来。

    MyBatis延迟加载是通过动态代理实现的,当调用配置为延迟加载的属性方法时(如getXXX()),此时会调用动态代理对象的get方法,会发送sql到db查询数据,这些操作是通过SqlSession来执行的。由于在和某些框架集成时,SqlSession的生命周期交给了框架来管理,因此当对象超出SqlSession生命周期调用时,会由于链接关闭等问题而抛出异常。因而在与Spring集成时,需要注意SqlSession的声明周期。

    8.1 延迟加载的优缺点

    • 优点:先从单表查询,需要时再去查询另一张表(两次查询),如果并没有用到另一张表中的数据,可以加快查询速度。
      • 如果使用延迟加载,使用关联查询的话,在数据量很大的情况下,关联查询jion两张大表,效率会很低。而如果延迟加载第二次延迟查询命中索引概率大的话,效率会更高。
      • 一定要进行关联查询吗?
        • 关联查询效率很低,尤其是两张大表jion的情况下,所以要尽可能避免这种情况!
        • 如果非要两张大表jion的话,可以不作为实时场景,让它作为一个定时任务去跑,第一次跑的数据量大可能耗时长,后面采用按时间增量更新的策略,根据时间切分的好后面每次跑的数据量就不会太大。
        • 如果一张大表跟一张小表jion,那么可以将小表缓存,单查一张大表将查询得到的结果(肯定比全量jion数据要少)再去jion。
        • 大表单查很慢,可以多个线程去查,如果大表做了分库分表或者按时间分区,查询方式就又有不同。
        • 尽量将一个大sql拆分为多个小sql,大sql会长时间占用连接,影响其他sql。
    • 缺点:因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降。

    9. 如何将jdbc查询的结果转换为相应对象的?

    1. 当调用SimpleExecutor#query()方法时,如果没有命中缓存,则会queryFromDatabase,最终会使用如PreparedStatement执行查询;
    2. ResultSetHandler#handleResultSets()方法,将resultSet转为对应的对象。
      a. 获取resultSet中的所有列名,并获得列名对应的值;
      b. 查找是否有匹配的TypeHandler,如果有的话,调用TypeHandler的getResult() → getNullableResult()方法,获得通过TypeHandler转换后的属性值;
      c. 利用反射new对象,根据列名获得对象的setXX()方法,再使用反射调用待生成对象的setXX()方法,将属性设置进去;
      d. 将设置了属性的对象存入ResultHandler中;
      e. 这样仅仅是处理了一条记录,如果查询结果有多条记录,还会循环这个过程。
      f. 利用ResultHandler处理对象。
      i. 如DefaultResultHandler,内部维护了一个List,查询得到的n条记录都会存在这里
      ii. 最终返回的数据会判断List中的个数,如果只有一个就get(0),只返回一个对象。如果有多个会返回这个List

    相关文章

      网友评论

          本文标题:MyBatis源码学习

          本文链接:https://www.haomeiwen.com/subject/ztzebctx.html