深入理解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.延迟加载策略总结

参照:动力节点

posted @ 2019-06-02 22:46  白晨冬阳  阅读(269)  评论(0编辑  收藏  举报