MyBatis
JDBC
- 加载驱动 Class.forName(“com.mysql.jdbc.Driver”)
- 获取链接 Connection conn = (Connection) DriverManager.getConnection(url, username, password)
- 执行PreparedStatement
- 返回结果集 ResultSet
Mybatis总体框架设计
架构概览
接口层 -和数据库交互的方式
MyBatis和数据库的交互有两种方式:
- 使用传统的MyBatis提供的API
- 使用Mapper接口
数据处理层
数据处理层可以说是MyBatis的核心,从大的方面上讲,它要完成两个功能:
- 通过传入参数构建动态SQL语句
- SQL语句的执行以及封装查询结果集成
List<E>
参数映射和动态SQL语句生成
动态语句生成可以说是MyBatis框架非常优雅的一个设计,MyBatis通过传入的参数值,使用Ognl来动态地构造SQL语句,使得MyBatis有很强的灵活性和扩展性。
参数映射指的是对于java数据类型和jdbc数据类型之间的转换。
这里有包括两个过程:
- 查询阶段,我们要将java类型的数据,转换成jdbc类型的数据,通过 preparedStatement.setXXX() 来设值;
- 对resultset查询结果集的jdbcType 数据转换成java 数据类型。
SQL语句的执行以及封装查询结果集成List
动态SQL语句生成之后,MyBatis 将执行SQL语句,并将可能返回的结果集转换成List
MyBatis 在对结果集的处理中,支持结果集关系一对多和多对一的转换,并且有两种支持方式,一种为嵌套查询语句的查询,还有一种是嵌套结果集的查询。
框架支撑层
- 事务管理机制
事务管理机制对于ORM框架而言是不可缺少的一部分,事务管理机制的质量也是考量一个ORM框架是否优秀的一个标准。 - 连接池管理机制
由于创建一个数据库连接所占用的资源比较大,对于数据吞吐量大和访问量非常大的应用而言,连接池的设计就显得非常重要。 - 缓存机制
为了提高数据利用率和减小服务器和数据库的压力,MyBatis 会对于一些查询提供会话级别的数据缓存,会将对某一次查询,放置到SqlSession 中,在允许的时间间隔内,对于完全相同的查询,MyBatis 会直接将缓存结果返回给用户,而不用再到数据库中查找。 - SQl语句的配置方式
Mapper + XML
主要构件及其相互关系
- SqlSession,作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能;
- Executor,MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护;
- StatementHandler,封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合;
- ParameterHandler,负责对用户传递的参数转换成JDBC Statement 所需要的参数;
- ResultSetHandler,负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
- TypeHandler 负责java数据类型和jdbc数据类型之间的映射和转换;
- MappedStatement MappedStatement维护了一条select|update|delete|insert节点的封装;
- SqlSource 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回;
- BoundSql 表示动态生成的SQL语句以及相应的参数信息;
- Configuration MyBatis所有的配置信息都维持在Configuration对象之中。
MyBatis执行流程
Mybatis核心流程四大对象
MyBatis完成一次数据库操作需要经过的步骤,如下:
- 加载配置文件,获取SqlSessionFactoryBuiler对象
- 通过SqlSessionFactoryBuiler和配置文件流来获取SqlSessionFactory对象
- 利用SqlSessionFactory对象来打开一个SqlSession
- 通过SqlSession来获得对应的Mapper对象
- 通过Mapper对象调用对应接口来封装执行sql并解析返回数据
对象 | 生命周期 |
---|---|
SqlSessionFactoryBuiler | 方法局部(Method)使用完成即可被丢弃 |
SqlSessionFactory | 应用级别(Application),全局存在,是一个单例对象 |
SqlSession | 请求或方法(Request / Method) |
Mapper | 方法(Method) |
SqlSession的创建过程
- 首先,SqlSessionFactoryBuilder去读取mybatis的配置文件,然后build一个DefaultSqlSessionFactory。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28/**
* 一系列的构造方法最终都会调用本方法(配置文件为Reader时会调用本方法,还有一个InputStream方法与此对应)
* @param reader
* @param environment
* @param properties
* @return
*/
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
//通过XMLConfigBuilder解析配置文件,解析的配置相关信息都会封装为一个Configuration对象
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
//这儿创建DefaultSessionFactory对象
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
} - 获取到SqlSessionFactory之后,就可以通过SqlSessionFactory去获取SqlSession对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25/**
* 通常一系列openSession方法最终都会调用本方法
* @param execType
* @param level
* @param autoCommit
* @return
*/
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
//通过Confuguration对象去获取Mybatis相关配置信息, Environment对象包含了数据源和事务的配置
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//之前说了,从表面上来看,咱们是用sqlSession在执行sql语句, 实际呢,其实是通过excutor执行, excutor是对于Statement的封装
final Executor executor = configuration.newExecutor(tx, execType);
//关键看这儿,创建了一个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();
}
}- 从配置中获取Environment;
- 从Environment中取得DataSource;从Environment中取得TransactionFactory;
- 从DataSource里获取数据库连接对象Connection;在取得的数据库连接上创建事务对象Transaction;
- 创建Executor对象(该对象非常重要,事实上sqlsession的所有操作都是通过它完成的);
- 创建sqlsession对象。
Mapper执行流程
Mapper执行一次sql需要经过的步骤,如下:
- 通过MybatisMapperRegistry获取JDK动态代理对象MybatisMapperProxy
- 通过MybatisMapperProxy执行代理方法
- 通过MappedStatement和入参创建Executor并实际执行数据库操作
在mybatis中,通过MapperProxy动态代理咱们的dao或者mapper接口,也就是说,当咱们执行自己写的dao里面的方法的时候,其实是对应的mapperProxy在代理。
那么,看看怎么获取MapperProxy对象吧。
- SqlSession
SqlSession把包袱甩给了Configuration1
2
3
4
5
6
7/**
* 什么都不做,直接去configuration中找, 哥就是这么任性
*/
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
} - Configuration
Configuration不要这烫手的山芋,接着甩给了MapperRegistry1
2
3
4
5
6
7
8
9/**
* 烫手的山芋,俺不要,你找mapperRegistry去要
* @param type
* @param sqlSession
* @return
*/
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
} - MapperRegistry
MapperRegistry交给MapperProxyFactory去做1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/**
* 烂活净让我来做了,没法了,下面没人了,我不做谁来做
* @param type
* @param sqlSession
* @return
*/
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//能偷懒的就偷懒,俺把粗活交给MapperProxyFactory去做
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
//关键在这儿
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
} - MapperProxyFactory
1
2
3
4
5
6
7
8
9
10
11
12
13
14/**
* @param mapperProxy
* @return
*/
protected T newInstance(MapperProxy<T> mapperProxy) {
//动态代理我们写的dao接口
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
Executor执行流程
Executor执行过程需要经过的步骤,如下:
- 创建Executor,然后利用StatementHandler执行数据库操作
- 执行数据库操作前,利用ParameterHandler做参数处理
- 执行数据库操作后,利用ResultSetHandler处理数据库返回结果
Executor与Sqlsession的关系就像市长与书记,Sqlsession只是个门面,真正干事的是Executor。
Sqlsession对数据库的操作都是通过Executor来完成的。
与Sqlsession一样,Executor也是动态创建的:
1 | /** |
StatementHandler sql执行、参数及结果映射流程
- 将参数和执行sql解析封装成MetaObject
- 执行sql前后通过TypeHandler对java与数据库参数进行映射
MyBatis一级、二级缓存
一级缓存
Mybatis对缓存提供支持,但是在没有配置的默认情况下,它只开启一级缓存,一级缓存只是相对于同一个SqlSession
而言。
一级缓存的生命周期
- MyBatis在开启一个数据库会话时,会创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象。Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
- 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。
- 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。
- SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用。
怎么判断某两次查询是完全相同的查询
Mybatis认为,对于两次查询,如果以下条件都完全一样,那么就认为它们是完全相同的两次查询。
- 传入的statementId。
- 查询时要求的结果集中的结果范围。
- 这次查询所产生的最终要传递给JDBC java.sql.Preparedstatement的Sql语句字符串(boundSql.getSql() )。
- 传递给java.sql.Statement要设置的参数值。
总结
- MyBatis一级缓存的生命周期和SqlSession一致。
- MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。
- MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。
1
2
3
4
5mybatis:
configuration:
cache-enabled: false #禁用二级缓存
local-cache-scope: statement #一级缓存指定为statement级别。每次查询结束都会清掉一级缓存,实际效果就是禁用了一级缓存。
local-cache-scope: session #一级缓存指定为session级别。对同样的查询将不再查询数据库,直接从缓存中获取。
二级缓存
MyBatis的二级缓存是Application级别的缓存。
范围是按照每个namepace缓存来存贮和维护,同一个namespace放到一个缓存对象中。
当这个namaspace中执行了insert、update和delete语句的时候,整个namespace中的缓存全部清除掉。
1 | mybatis: |
总结
- MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
- MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
- 在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。