JDBC

  1. 加载驱动 Class.forName(“com.mysql.jdbc.Driver”)
  2. 获取链接 Connection conn = (Connection) DriverManager.getConnection(url, username, password)
  3. 执行PreparedStatement
  4. 返回结果集 ResultSet

Mybatis总体框架设计

架构概览

接口层 -和数据库交互的方式

MyBatis和数据库的交互有两种方式:

  • 使用传统的MyBatis提供的API
  • 使用Mapper接口

数据处理层

数据处理层可以说是MyBatis的核心,从大的方面上讲,它要完成两个功能:

  • 通过传入参数构建动态SQL语句
  • SQL语句的执行以及封装查询结果集成List<E>

参数映射和动态SQL语句生成
动态语句生成可以说是MyBatis框架非常优雅的一个设计,MyBatis通过传入的参数值,使用Ognl来动态地构造SQL语句,使得MyBatis有很强的灵活性和扩展性。

参数映射指的是对于java数据类型和jdbc数据类型之间的转换。
这里有包括两个过程:

  1. 查询阶段,我们要将java类型的数据,转换成jdbc类型的数据,通过 preparedStatement.setXXX() 来设值;
  2. 对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完成一次数据库操作需要经过的步骤,如下:

  1. 加载配置文件,获取SqlSessionFactoryBuiler对象
  2. 通过SqlSessionFactoryBuiler和配置文件流来获取SqlSessionFactory对象
  3. 利用SqlSessionFactory对象来打开一个SqlSession
  4. 通过SqlSession来获得对应的Mapper对象
  5. 通过Mapper对象调用对应接口来封装执行sql并解析返回数据
对象 生命周期
SqlSessionFactoryBuiler 方法局部(Method)使用完成即可被丢弃
SqlSessionFactory 应用级别(Application),全局存在,是一个单例对象
SqlSession 请求或方法(Request / Method)
Mapper 方法(Method)

SqlSession的创建过程

创建SqlSessiond的过程

  1. 首先,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);
    }
  2. 获取到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();
    }
    }
    1. 从配置中获取Environment;
    2. 从Environment中取得DataSource;从Environment中取得TransactionFactory;
    3. 从DataSource里获取数据库连接对象Connection;在取得的数据库连接上创建事务对象Transaction;
    4. 创建Executor对象(该对象非常重要,事实上sqlsession的所有操作都是通过它完成的);
    5. 创建sqlsession对象。

Mapper执行流程

Mapper执行一次sql需要经过的步骤,如下:

  1. 通过MybatisMapperRegistry获取JDK动态代理对象MybatisMapperProxy
  2. 通过MybatisMapperProxy执行代理方法
  3. 通过MappedStatement和入参创建Executor并实际执行数据库操作

MapperProxy的创建过程

在mybatis中,通过MapperProxy动态代理咱们的dao或者mapper接口,也就是说,当咱们执行自己写的dao里面的方法的时候,其实是对应的mapperProxy在代理。
那么,看看怎么获取MapperProxy对象吧。

  1. SqlSession
    SqlSession把包袱甩给了Configuration
    1
    2
    3
    4
    5
    6
    7
    /**
    * 什么都不做,直接去configuration中找, 哥就是这么任性
    */
    @Override
    public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);
    }
  2. Configuration
    Configuration不要这烫手的山芋,接着甩给了MapperRegistry
    1
    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);
    }
  3. 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
    */
    @SuppressWarnings("unchecked")
    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);
    }
    }
  4. MapperProxyFactory
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /**
    * @param mapperProxy
    * @return
    */
    @SuppressWarnings("unchecked")
    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执行过程需要经过的步骤,如下:

  1. 创建Executor,然后利用StatementHandler执行数据库操作
  2. 执行数据库操作前,利用ParameterHandler做参数处理
  3. 执行数据库操作后,利用ResultSetHandler处理数据库返回结果

Executor与Sqlsession的关系就像市长与书记,Sqlsession只是个门面,真正干事的是Executor。
Sqlsession对数据库的操作都是通过Executor来完成的。
与Sqlsession一样,Executor也是动态创建的:
Excutor的创建过程

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
29
30
31
32
/**
* Configuration#newExecutor
* Executor创建的源代码
*/
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ?ExecutorType.SIMPLE : executorType;
Executor executor;
if(ExecutorType.BATCH == executorType) { // BatchExecutor专门用于执行批量sql操作
executor = new BatchExecutor(this,transaction);
} else if(ExecutorType.REUSE == executorType) { // ReuseExecutor会重用statement执行sql操作
executor = new ReuseExecutor(this,transaction);
} else {
executor = newSimpleExecutor(this, transaction); // SimpleExecutor只是简单执行sql没有什么特别的
}
if (cacheEnabled) {
// 开启cache的话(默认是开启的并且没有任何理由去关闭它),就会创建CachingExecutor,它以前面创建的Executor作为唯一参数
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}

/**
* Configuration#newStatementHandler
* newStatementHandler创建的源代码
*/
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}

StatementHandler sql执行、参数及结果映射流程

  1. 将参数和执行sql解析封装成MetaObject
  2. 执行sql前后通过TypeHandler对java与数据库参数进行映射

MyBatis一级、二级缓存

美团技术团队-聊聊MyBatis缓存机制

一级缓存

Mybatis对缓存提供支持,但是在没有配置的默认情况下,它只开启一级缓存,一级缓存只是相对于同一个SqlSession而言。

一级缓存的生命周期

  1. MyBatis在开启一个数据库会话时,会创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象。Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
  2. 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。
  3. 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。
  4. SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用。

怎么判断某两次查询是完全相同的查询
Mybatis认为,对于两次查询,如果以下条件都完全一样,那么就认为它们是完全相同的两次查询。

  1. 传入的statementId。
  2. 查询时要求的结果集中的结果范围。
  3. 这次查询所产生的最终要传递给JDBC java.sql.Preparedstatement的Sql语句字符串(boundSql.getSql() )。
  4. 传递给java.sql.Statement要设置的参数值。

总结

  1. MyBatis一级缓存的生命周期和SqlSession一致。
  2. MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。
  3. MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。
    1
    2
    3
    4
    5
    mybatis:
    configuration:
    cache-enabled: false #禁用二级缓存
    local-cache-scope: statement #一级缓存指定为statement级别。每次查询结束都会清掉一级缓存,实际效果就是禁用了一级缓存。
    local-cache-scope: session #一级缓存指定为session级别。对同样的查询将不再查询数据库,直接从缓存中获取。

二级缓存

MyBatis的二级缓存是Application级别的缓存。
范围是按照每个namepace缓存来存贮和维护,同一个namespace放到一个缓存对象中。
当这个namaspace中执行了insert、update和delete语句的时候,整个namespace中的缓存全部清除掉。

1
2
3
4
mybatis: 
configuration:
// 默认不开启二级缓存
cache-enabled: false

总结

  1. MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
  2. MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
  3. 在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。