深入理解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.延迟加载策略总结
参照:动力节点