• 深入理解MyBatis(三)--运行源码解析及延迟加载


    GitHub:https://github.com/JDawnF

    一、运行源码解析

    先看一下Mybatis的Dao实现类例子,如下:

    A、 输入流的关闭

    在输入流对象使用完毕后,不用手工进行流的关闭。因为在输入流被使用完毕后,SqlSessionFactoryBuilder 对象的 build()方法会自动将输入流关闭。

    //SqlSessionFactoryBuilder.java
     public SqlSessionFactory build(InputStream inputStream) {
       return build(inputStream, null, null);
     }
     public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
         try {
           XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
           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.
           }
         }
       }

    B、 SqlSession 的创建

    SqlSession 接口对象用于执行持久化操作。一个 SqlSession 对应着一次数据库会话,一 次会话以 SqlSession 对象的创建开始,以 SqlSession 对象的关闭结束。

    SqlSession 接口对象是线程不安全的,所以每次数据库会话结束前,需要马上调用其 close()方法,将其关闭。再次需要会话,再次创建。而在关闭时会判断当前的 SqlSession 是否被提交:若没有被提交,则会执行回滚后关闭;若已被提交,则直接将 SqlSession 关闭。 所以,SqlSession 无需手工回滚。

    主要是一些增删改查的方法。

    SqlSession 对象的创建,需要使用 SqlSessionFactory 接口对象的 openSession()方法。 SqlSessionFactory 接口对象是一个重量级对象(系统开销大的对象),是线程安全的,所以一个应用只需要一个该对象即可。创建 SqlSession 需要使用 SqlSessionFactory 接口的的 openSession()方法。

    • openSession(true):创建一个有自动提交功能的 SqlSession

    • openSession(false):创建一个非自动提交功能的 SqlSession,需手动提交

    • openSession():同 openSession(false) ,即无参的openSession方法默认false是autoCommit的值

    SqlSessionFactory 接口的实现类为 DefaultSqlSessionFactory。

    
     
    // SqlSessionFactory.java
     public interface SqlSessionFactory {
       SqlSession openSession();
         // 多个openSession方法
       Configuration getConfiguration();
     }
     // DefaultSqlSessionFactory.java
     public SqlSession openSession() {
         // false是autoCommit的值,表示关闭事务的自动提交功能
         return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
      }
     //autoCommit表示是否自动提交事务
     private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
         Transaction tx = null;
         try {
             //读取Mybatis的主配置文件
           final Environment environment = configuration.getEnvironment();
             // 获取事务管理器transcationManager,比如配置文件中的JDBC
           final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
           tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
            // 创建执行器,传入的是事务和执行器类型(SIMPLE, REUSE, BATCH)
           final Executor executor = configuration.newExecutor(tx, execType);
           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();
         }
       }
     //DefaultSqlSession.java
     // 所谓创建SqlSession就是对一个dirty这个变量进行初始化,即是否为脏数据的意思
     public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
         // 对成员变量进行初始化
         this.configuration = configuration;
         this.executor = executor;
         this.dirty = false;     //  这个变量为false表示现在DB中的数据还未被修改
         this.autoCommit = autoCommit;
       }

    从以上源码可以看到,无参的 openSession()方法,将事务的自动提交直接赋值为 false。而所谓创建 SqlSession,就是加载了主配置文件,创建了一个执行器对象(将来用于执行映射文件中的 SQL 语句),初始化了一个 DB 数据被修改的标志变量 dirty,关闭了事务的自动提交功能。

    C、 增删改的执行

    对于 SqlSession 的 insert()、delete()、update()方法,其底层均是调用执行了 update()方法,只要对数据进行了增删改,那么dirty就会变为true,表示数据被修改了。

     // DefaultSqlSession.java
     public int insert(String statement, Object parameter) {
         return update(statement, parameter);
       }
     public int delete(String statement, Object parameter) {
         return update(statement, parameter);
       }
     public int update(String statement, Object parameter) {
         try {
           dirty = true;     //这里要开始修改数据了,所以要将dirty改为true,表示此时是脏数据
           // statement是获取映射文件中制定的sql语句,即mapper映射文件中的sql id
           MappedStatement ms = configuration.getMappedStatement(statement);
           return executor.update(ms, wrapCollection(parameter));
         } catch (Exception e) {
           throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
         } finally {
           ErrorContext.instance().reset();
         }
       }

    从以上源码可知,无论执行增、删还是改,均是对数据进行修改,均将 dirty 变量设置为了 true,且在获取到映射文件中指定 id 的 SQL 语句后,由执行器 executor 执行。

    D、 SqlSession 的提交 commit()

    // DefaultSqlSession.java
    public void commit() {
      commit(false);
    }
    public void commit(boolean force) {
        try {
            // 执行提交
          executor.commit(isCommitOrRollbackRequired(force));
          dirty = false;	// 提交之后把dirty设置为false,表示数据未修改
        } catch (Exception e) {
          throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
      }
    // 提交还是回滚
    /**当autoCommit为true时,返回false;
       当autoCommit为false,dirty为true时,返回true;
       当autoCommit为false,dirty为false时,如果force为true则返回true,为false则返回false
       在这里根据上面方法传过来的参数值,autoCommit为false,所以!false==true,dirty为true,force为	        	false,所以isCommitOrRollbackRequired返回true。
    */
    private boolean isCommitOrRollbackRequired(boolean force) {
        return (!autoCommit && dirty) || force;
      }
    // CachingExecutor.java
    // required根据上面的值是为true
    public void commit(boolean required) throws SQLException {
        delegate.commit(required);
        tcm.commit();
      }
    //BaseExecutor.java
    public void commit(boolean required) throws SQLException {
        if (closed) throw new ExecutorException("Cannot commit, transaction is already closed");
        clearLocalCache();
        flushStatements();
        if (required) {	// 根据上面返回的结果,required为true,提交事务
          transaction.commit();
        }
      }

    由以上代码可知,执行 SqlSession 的无参 commit()方法,最终会将事务进行提交。

    E、 SqlSession 的关闭

    //DefaultSqlSession.java
    public void close() {
      try {
          // 如果执行了commit方法,那么这里返回的是false,即close方法中传入的是false
        executor.close(isCommitOrRollbackRequired(false));
        dirty = false;
      } finally {
        ErrorContext.instance().reset();
      }
    }
    // 这里的force为false,autoCommit在最开始的openSession方法中传入的是为false,dirty在commit之后,而在commit方法中,将dirty设置为false了,所以这里dirty是false,所以这里整体返回的是false
    private boolean isCommitOrRollbackRequired(boolean force) {
        return (!autoCommit && dirty) || force;
      }
    //BaseExecutor.java
    public void close(boolean forceRollback) {
        try {
          try {
              // 根据上面传入的值,forceRollback为false
            rollback(forceRollback);
          } finally {	// 最后要确认事务关闭,如果前面执行了增删改查方法,说明提交了事务,所以事务不为空
            if (transaction != null) transaction.close();
          }
        } catch (SQLException e) {
          // Ignore.  There's nothing that can be done at this point.
          log.warn("Unexpected exception on closing transaction.  Cause: " + e);
        } finally {	//释放各种资源,并将关闭标志closed重置为true
          transaction = null;
          deferredLoads = null;
          localCache = null;
          localOutputParameterCache = null;
          closed = true;
        }
      }
    // 根据上面传进来的值,required为false
    public void rollback(boolean required) throws SQLException {
        if (!closed) {	// 此时还未关闭,所以closed为false,这里!closed为true
          try {
            clearLocalCache();
            flushStatements(true);
          } finally {
            if (required) {		// required为false,不会回滚事务
              transaction.rollback();
            }
          }
        }
      }

    从以上代码分析可知,在 SqlSession 进行关闭时,如果执行了commit,那么不会回滚事务;如果没有执行commit方法,那么就会回滚事务,那么数据不会插入到数据库。所以,对于MyBatis 程序,无需通过显式地对 SqlSession 进行回滚,达到事务回滚的目的。

    二、延迟加载

    MyBatis 中的延迟加载,也称为懒加载,是指在进行关联查询时,按照设置延迟规则推 迟对关联对象的 select 查询。延迟加载可以有效的减少数据库压力。 需要注意的是,MyBatis 的延迟加载只是对关联对象的查询有迟延设置,对于主加载对象都是直接执行查询语句的。

    Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载。其中,association 指的就是一对一,collection 指的就是一对多查询

    它的原理是,使用 CGLIB 或 Javassist( 默认 ) 创建目标对象的代理对象。当调用代理对象的延迟加载属性的 getting 方法时,进入拦截器方法。比如调用 a.getB().getName() 方法,进入拦截器的 invoke(...) 方法,发现 a.getB() 需要延迟加载时,那么就会单独发送事先保存好的查询关联 B 对象的 SQL ,把 B 查询上来,然后调用a.setB(b) 方法,于是 a 对象 b属性就有值了,接着完成a.getB().getName() 方法的调用。这就是延迟加载的基本原理。

    当然了,不光是 Mybatis,几乎所有的包括 Hibernate 在内,支持延迟加载的原理都是一样的。

    1.关联对象加载时机

    MyBatis 根据对关联对象查询的 select 语句的执行时机,分为三种类型:直接加载、侵 入式延迟加载与深度延迟加载。

    • 直接加载:执行完对主加载对象的 select 语句,马上执行对关联对象的 select 查询。

    • 侵入式延迟:执行对主加载对象的查询时,不会执行对关联对象的查询。但当要访问主加载对象的详情时,就会马上执行关联对象的 select 查询。即对关联对象的查询执行, 侵入到了主加载对象的详情访问中。也可以这样理解:将关联对象的详情侵入到了主加 载对象的详情中,即将关联对象的详情作为主加载对象的详情的一部分出现了。

    • 深度延迟:执行对主加载对象的查询时,不会执行对关联对象的查询。访问主加载对象 的详情时也不会执行关联对象的 select 查询。只有当真正访问关联对象的详情时,才会 执行对关联对象的 select 查询。

    需要注意的是,延迟加载的应用要求,关联对象的查询与主加载对象的查询必须是分别进行的 select 语句,不能是使用多表连接所进行的 select 查询。因为,多表连接查询,其实 质是对一张表的查询,对由多个表连接后形成的一张表的查询。会一次性将多张表的所有信 息查询出来。

    MyBatis 中对于延迟加载设置,可以应用到一对一、一对多、多对一、多对多的所有关 联关系查询中。

    2.直接加载

    修改主配置文件:在主配置文件的<properties/>与<typeAliases/>标签之间,添加<settings/>标签,用于完 成全局参数设置。

    延迟加载的相关参数名称及取值:

    全局属性 lazyLoadingEnabled 的值只要设置为 false,那么,对于关联对象的查询,将采 用直接加载。即在查询过主加载对象后,会马上查询关联对象。

    lazyLoadingEnabled 的默认值为 false,即直接加载。

    3.深度延迟加载

    修改主配置文件的<settings/>,将延迟加载开关 lazyLoadingEnabled 开启(置为 true), 将侵入式延迟加载开关 aggressiveLazyLoading 关闭(置为 false)。

    4.侵入式延迟加载

    修改主配置文件的<settings/>,将延迟加载开关 lazyLoadingEnabled 开启(置为 true), 将侵入式延迟加载开关 aggressiveLazyLoading 也开启(置为 true,默认为 true)。

    该延迟策略使关联对象的数据侵入到了主加载对象的数据中,所以称为 侵入式延迟加载。 需要注意的是,该延迟策略也是一种延迟加载,需要在延迟加载开关 lazyLoadingEnabled 开启时才会起作用。若 lazyLoadingEnabled 为 false,则 aggressiveLazyLoading 无论取何值, 均不起作用。

    5.延迟加载策略总结

    参照:动力节点

  • 相关阅读:
    (十四)配置的热更新
    (十三)在ASP.NET CORE中使用Options
    (十二)Bind读取配置到C#实例
    【转载】ViewState的用法
    【转载】Web Service和WCF的到底有什么区别
    【转载】小小的公共库,大大的耦合,你痛过吗?
    【转载】ASP.NET应用程序与页面生命周期
    【转载】分布式数据库架构--分库、分表、排序、分页、分组、实现
    【转载】ASP和ASP.NET根本区别
    【转载】聊一聊C#的Equals()和GetHashCode()方法
  • 原文地址:https://www.cnblogs.com/baichendongyang/p/13235436.html
Copyright © 2020-2023  润新知