sqlSessionFactory与SqlSession
从表面上看,我们都是通过SqlSession
去执行sql语句的。
SqlSession
对应着一次数据库的会话,由于数据库会话不是永久的,因此SqlSession
的生命周期也不是永久的,每次访问数据库时都需要创建它(当然不是说在SqlSession
里只能执行一次sql,可以执行多次,当一旦关闭了SqlSession
就需要重新创建它)。
- 首先,
SqlSessionFactoryBuilder
去读取mybatis的配置文件,然后build一个DefaultSqlSessionFactory
。 - 当我们获取到
SqlSessionFactory
之后,就可以通过SqlSessionFactory
去获取SqlSession
对象,创建SqlSession
的地方只有一个,那就是SqlSessionFactory
的openSession
方法,创建SqlSession
经过以下几个步骤:
(1)从配置中获取Environment
(2)从Environment获取DataSource
(3)从Environment获取TransactionFactory
(4)从DataSource里获取数据库连接对象Connection
(5)在取得的数据库连接上创建事务对象Transaction
(6)创建Executor对象(该对象非常的重要,事实上SqlSession的所有操作都是通过Executor来完成的)
(7)创建SqlSession对象 - 拿到
SqlSession
之后,可以调用SqlSession
中一系列select...、insert...、update...、delete...
方法进行CRUD操作了。
动态代理
在了解下面的MapperProxy
之前,我们先来了解一下什么是动态代理。但是在介绍动态代理之前,先介绍一下什么是代理模式,当我们不想访问或者不能直接访问一个对象的时候,就需要用到代理模式,代理模式一般涉及到委托类和代理类两个概念。代理类用于为委托类处理一些事务,代理类对象常常与委托类对象相关联。
代理模式可以分为静态代理和动态代理。静态代理需要为每一个委托类实现一个代理类,程序运行之前代理类的.class文件就已经存在了;而动态代理则是在运行时利用反射机制动态生成的。
动态代理的应用:
Spring的AOP、加事务、加权限、加日志。
当想要给实现了某个接口的类中的方法加一些额外的处理,比如说加日志、加事务等,可以给这个类创建一个代理,也就是创建一个新的类,这个类不仅包含了原来类方法的功能,而且还在原来的基础上添加了额外处理的新类。
动态代理的实现:
首先必须定义一个接口,还要有一个InvocationHandler
处理类(将实现接口的类的对象传递给它);再有一个工具类Proxy
(代理类,调用它的newInstance()
可以产生代理对象,其实它只是一个产生代理对象的工具类);
动态代理的类实现了InvocationHandler
接口,定义Object
对象为委托类的对象,而实际执行的方法是在invoke()
方法中完成的,此时我们不用再关心代理的对象是谁,只需要实现一套逻辑即可。
- 定义一个
Subject
接口,主要用于声明真实对象要让代理对象做的事情:
public interface Subject {
void buy();
}
- 创建委托类:
public class RealSubject implements Subject {
@Override
public void buy() {
System.out.println("我要买口红");
}
}
- 创建动态代理类:
public class DynamicProxy implements InvocationHandler {
//object为委托类对象
private Object object;
public DynamicProxy(Object object) {
this.object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//调用委托类对象的方法
Object result = method.invoke(object, args);
return result;
}
}
- 测试类:
public class TestProxy {
public static void main(String[] args) {
//创建委托类
Subject subject = new RealSubject();
Subject dynamicProxy = (Subject) Proxy.newProxyInstance(
subject.getClass().getClassLoader(),
subject.getClass().getInterfaces(),
new DynamicProxy(subject));
dynamicProxy.buy();
}
}
MapperProxy
在MyBatis中,通过MapperProxy
动态代理dao
,也就是说,当我们执行自己写的dao
里面的方法的时候,其实是对应的MapperProxy
在代理。
通过SqlSession
从Configuration
中获取MapperProxy
对象。通过动态代理,就可以方便的使用dao
接口了。
Excutor
Excutor
和SqlSession
的关系就像是市长和书记,SqlSession
只是个门面,真正干事的是Excutor
,SqlSession
对数据库的操作都是通过Excutor
来完成的,与SqlSession
一样,Excutor
也是动态创建的:
如果不开启cache
的话,创建的Excutor
只是3个基础类型之一,BatchExecutor
专门用于执行批量sql操作。ReuseExecutor
会重用statement
执行sql操作,SimpleExecutor
只是简单执行sql。
如果开启cache
的话(默认是开启的),就会创建CachingExecutor
,它以前面创建的Executor
作为唯一参数。CachingExecutor
在查询数据库之前先查找缓存,若没找到的话调用delegate
(就是构造时传入的Executor
对象)从数据库查询,并将查询结果存入缓存中。
Executor
对象是可以被插件拦截的,如果定义了针对Executor
类型的插件,最终生成的Executor
对象是被各个插件插入后的代理对象。
sql的执行过程
拿到了MapperProxy
之后,每个MapperProxy
对应一个dao
接口,MapperProxy
的执行过程:
对被代理对象的访问都会落实到代理者的invoke
上来,MapperProxy
在执行时会触发此方法,源码如下:
/**
* MapperProxy在执行时会触发此方法
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//如果是Object类的方法
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
//二话不说,主要交给MapperMethod自己去管
return mapperMethod.execute(sqlSession, args);
}
其中的MapperMethod
就像是一个分发者,他根据参数和返回值类型选择不同的sqlsession
方法来执行,这样mapper
对象与sqlsession
就真正的关联起来了。又回到了sqlsession
,前面提到过,sqlsession
只是一个门面,真正发挥作用的是executor
,对sqlsession
方法的访问最终都会落到executor
的相应方法上去。executor
分为两大类:一类是CacheExecutor
,一类是Executor
。
-
CacheExecutor
CacheExecutor
有一个重要属性delegate
,他保存的是某类普通的Executor
,值在构造时传入。执行数据库update
操作时,他直接调用delegate
的update
方法,执行query
方法时先尝试在cache
中取值,取不到再调用delegate
的查询方法,并将查询结果存入cache
中。 -
普通
Executor
有三类,他们都继承于BaseExecutor
,默认是SimpleExecutor
,BatchExecutor
专门用于执行批量sql操作,ReuseExecutor
会重用statement
执行sql操作,SimpleExecutor
只是简单的执行sql没有什么特别的,该模式下它为每个语句的执行创建一个新的预处理语句,单条提交sql;而BatchExecutor
重复使用已经预处理的语句,并且批量执行所有更新语句,显然batch性能更优。
但是batch也有自己的问题,比如在insert操作时,在事务没有提交之前,是没有办法获取到自增的id,这在某些情形下是不符合业务要求的。在同一事务中batch模式和simple模式之间无法转换。
StatementHandler
Executor
本质上也是一个甩手掌柜,具体的事情是由StatementHandler
来完成的。每次创建的StatementHandler
都是RoutingStatementHandler
,它只是一个分发者,它的一个属性delegate
用于指定用哪种具体的StatementHandler
,可选的StatementHandler
有三种:SimpleStatementHandler、PreparedStatementHandler、CallableStatementHandler
,选用哪种在mapper配置文件的每个statement
里指定,默认的是PreparedStatementHandler
。StatementHandler
可以被拦截器拦截,和Executor
一样,被拦截器拦截后的对象是一个代理对象。由于MyBatis没有实现数据库的物理分页,众多物理分页的实现都是在这个地方使用拦截器实现的。
StatementHandler
创建后需要执行一些初始操作,比如statement的开启和参数设置、对于PreparedStatement还需要执行参数的设置操作等,代码如下:
private Statement prepareStatement(StatementHandler handler) throws SQLException {
Statement stmt;
Connection connection = transaction.getConnection();
stmt =handler.prepare(connection);
handler.parameterize(stmt);
return stmt;
}
handler.parameterize
通过调用ParameterHandler
的setParameters
完成参数的设置,ParameterHandler
随着 StatementHandler
的创建而创建,默认的实现是DefaultParameterHandler
。
同Executor
和 StatementHandler
一样,ParameterHandler
也是可以被拦截的。设置参数的完成实际上使用合适的TypeHandler
来完成的。BaseStatementHandler
的构造方法中有这么一句:
this.boundSql= mappedStatement.getBoundSql(parameterObject);
它触发了sql的解析,在解析sql的过程中,TypeHandler
也被决断出来了,决断的原则是根据参数的类型和参数对应的JDBC类型决定使用哪个TypeHandler
。
参数设置完毕之后,执行数据库操作(update或query),如果是query最后还有个查询结果的处理过程。
结果处理使用ResultSetHandler
来完成的,默认的ResultSetHandler
是FastResultSetHandler
,他在创建StatementHandler
时一起被创建。ResultSetHandler
也是可以被拦截的,可以编写自己的拦截器改变ResultSetHandler
的默认行为。ResultSetHandler
内部一条记录一条记录的处理,在处理每条记录的每一列时会调用TypeHandler
转换结果,决断TypeHandler
使用的是结果参数的属性类型。
感谢并参考:
https://pdai.tech/md/framework/orm-mybatis/mybatis-y-sql-exec.html