开篇
mybatis版本:3.5.12
JDBC视角看数据库操作
我们知道,MyBatis是对JDBC的封装。让程序员从复杂繁琐的JDBC编程中解放双手。简单回顾一下JDBC编程的过程。通常分为这么几步:
- 加载驱动
- 获取连接
Connection
对象 - 从Connection中获取
Statement
对象(或者是PreparedStatement
)对象 - 用户准备SQL语句
- 使用Statement对象执行SQL语句获得结果集对象——
ResultSet
- 解析ResultSet对象,从ResultSet中获取需要的值。
- 关闭资源
以上7个步骤之后就统称传统JDBC了。传统JDBC的缺点太多了:其中1、2、3、5、6、7都是重复性的工作。JDBC中真正由用户确定的核心逻辑是SQL,理想中的对数据库的操作应该是简洁的。用户提供SQL并指定返回的结果集类型,程序执行SQL并自动返回解析后的结果集。
当然JDBC也有其他的缺点,综合来讲主要缺点为:
- 对事务的控制管理
- 连接资源的管理
- 代码复用性
- 其他高级设置(缓存等)
不过JDBC的缺陷就是天生的,Java只提供原生的操作,无论框架再怎么简单易用,底层的操作还是最基本的JDBC。
MyBatis解决了上述JDBC编程中的问题。对JDBC进行了封装。MyBatis框架的核心思想就是:用户提供SQL和返回值类型。其他的什么比如是否缓存、资源何时关闭、事务控制等全部交给框架来做。用户只需要做两件事——提供SQL和返回值类型。
MyBatis视角看数据库操作
在MyBatis中,既然用户解放了,那框架一定干的活多了。接下来就来探讨下MyBatis内部是如何封装那些重复性的操作的。从MyBatis的角度看,执行一条SQL需要经过这么几个步骤
- 读取数据库配置(账号、密码、文件资源位置等)
- 获取会话对象(
SqlSession
,相当于一个命令行黑窗口界面。可以操作SQL语句) - 执行用户提供的SQL并返回结果集(包含Mapper方式)
- 自动释放资源
下面是代码描述
// 1. 读取数据库配置(账号、密码、文件资源位置等)
InputStream is = Resources.getResourceAsStream("sqlMapConfig.xml");
// 2. 获取会话对象(SqlSession,相当于一个命令行黑窗口界面。可以操作SQL语句)
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = factory.openSession();
// 3. 执行用户提供的SQL并返回结果集。(Mapper使用方式后面介绍)
List<Object> userList = sqlSession.selectList("org.apache.ibatis.amy.mapper.UserMapper.selectUser");
// 4. 自动释放资源(既然是自动的,用户就无需写代码了
复制代码
注:接下来,整个文章将对MyBatis的这4个步骤展开详细讨论。本系列文章本着由浅入深的理念将逐步揭开MyBatis的面纱。首先从应用入手,MyBatis是如何执行SQL的,如何获取返回的结果集,了解了执行SQL的大致过程后,再带着问题逐步深挖源码。接下来就是探究配置文件是如何被加载的,又是如何被使用的。最后介绍MyBatis的一些扩展点,插件机制、ObjectFactory等。
SQL如何执行
通过开篇,我们已经了解到MyBatis只需要用户提供SQL而无需其他操作就可以完成对数据库的查询。那么SQL到底是如何被MyBatis执行的呢?
大致流程
在看源码前先简单介绍一下几个重要对象以及他们之间的关系。
Class | 作用 |
---|---|
SqlSession | SqlSession提供了CRUD的方法,通过调用select/update/insert/delete方法(参数是SQL存储位置),就可以完成对数据库的查询 |
Executor | 执行器;SqlSession中的内部属性。SqlSession会委托Executor来执行SQL语句。还包括一些复杂操作。比如缓存等,就是在Executor中完成的。 |
StatementHandler | SqlSession的方法参数并不是直接的SQL,而是SQL存储的位置。那么Executor执行的SQL语句就是由StatementHandler根据指定位置解析出来的。 |
ResultSetHandler | ResultSetHandler;用来处理结果集对象。比如Select user_name from user; 其中结果集中的user_name和实体User的userName属性关联,就是由ResultSetHandler完成的 |
TypeHandler | JDBC类型——Java类型的转换 |
ParameterHandler | PreparedStatement的参数设置 |
交接了大概执行步骤后,接下来带着这个思路看源码。
具体流程
我们借用开篇的示例来看一下
InputStream is = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = factory.openSession();
List<Object> userList = sqlSession.selectList("org.apache.ibatis.amy.mapper.UserMapper.selectUser");
// 后续用户自己的操作
复制代码
主要关注下最后一行代码。调用SqlSession对象提供的selectList方法,参数是SQL的位置(本例中指org.apache.ibatis.amy.mapper包下的UserMapper.xml文件中的selectUser标签)。
SqlSession#selectList方法内部会根据参数找到具体的SQL位置。
SqlSession#selectList方法
SqlSession是一个接口类,它有2个实现类。我们关注DefaultSqlSession即可。selectList有很多重载方法,最终都会调用到如下这个方法,接下来看下DefaultSqlSession#selectList的主要逻辑(省略非核心代码)
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
// 1. 通过配置对象获取MappedStatement对象,MappedStatement中包含了解析后的SQL语句。
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
}
复制代码
可以看到selectList方法只做了两件事
-
通过配置对象获取MappedStatement对象,MappedStatement中包含了解析后的SQL语句。(ms对象的具体实现现在不必关心,只需要知道它其中封装好了SQL即可)为了方便理解,不妨给他起个名字——后面我们就称它为sql包装对象
-
executor是DefaultSqlSession中的一个属性。它通过调用query方法,根据sql包装对象(MappedStatement) 和用户提供的参数来查询数据库。
Executor#query方法
Executor也是一个接口对象。它提供了一系列的方法(query/update)方法完成对数据库的CRUD**(查询是query方法,增删改都是update方法)**
它有接口体系如下
看源码的过程中(注意是看源码的过程中哦)最常用的是SimpleExecutor
和BaseExecutor
。我们只需要关注这两个类的方法实现就好了。而query方法实在BaseExecutor
中实现的。它也有很多重载的方法,但是最终都会调用到一个query方法。下面来看一下BaseExecutor#query
方法的核心逻辑(非核心代码省略)
// RowBounds是内存分页对象。几乎没什么使用场景。忽略该对象即可
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 1. BoundSql存储的就是可执行的SQL和用户参数
BoundSql boundSql = ms.getBoundSql(parameter);
// 2. 创建缓存key,把它当成复杂的Map数据结构的的key值就行了。
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 3. 最终都会调用到这个query的重载方法中
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 1. 从一级缓存中获取对象(第一次肯定是没有的)
List<E> list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// (存储过程相关)
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 2. 没查到缓存就查数据库
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
return list;
}
复制代码
我们可以看到Executor#query方法的大致执行逻辑是
- 从sql包装对象(MappedStatement) 中获取真实的SQL
- 根据sql和一系列参数创建缓存的key值。该key值能在一级缓存中唯一确定一个对象。
- 根据CacheKey(缓存key)从一级缓存中获取查询结果。第一次执行该方法,缓存中肯定没有。
- 缓存中不存在该SQL的执行结果,则查询数据库。
查询数据库的操作是通过queryFromDatabase
方法完成的。接下来看一下BaseExecutor#queryFromDatabase
的具体实现
BaseExecutor#queryFromDatabase
queryFromDatabase;故名思意,该方法是查询数据库返回结果。该方法也是在BaseExecutor中实现的,接下来来看一下它的核心代码(非核心省略)
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 1. 缓存占位
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 2. 又调用doQuery查询
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 3. 移除缓存占位
localCache.removeObject(key);
}
// 4. 查询的结果添加到缓存中
localCache.putObject(key, list);
// 省略存储过程相关代码
return list;
}
复制代码
根据代码,我们来简单介绍下queryFromDatabase方法做了哪些事。
- 先把一个占位对象放入到一级缓存中。(有点类似于占座位)。
- 调用
doQuery
方法执行查询数据库的逻辑(后面重点分析)。 - 完成数据库查询后,从一级缓存中删除占位符。
- 真正的把查询结果缓存到一级缓存中。
该方法的逻辑比较简单,接下来就来看真正执行数据库的方法doQuery吧!
SimpleExecutor#doQuery
doQuery方法的具体实现是交给子类的,我们平时用到的也就是SimpleExecutor,接下来来看下SimpleExecutor是如何实现doQuery方法的
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 获取StatementHandler对象,可以通过StatementHandler获取JDBC中的Statement对象
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
// 底层实际上是通过Statement执行SQL的
return handler.query(stmt, resultHandler);
} finally {
// 释放资源
closeStatement(stmt);
}
}
复制代码
我们来简单介绍下该方法的步骤
- 通过Configuration(这是全局配置对象,存储了数据库密码、账户、超时时间等各种配置信息)获取StatementHandler对象。
- 通过prepareStatement方法获取Statement对象。有木有激动!终于看到JDBC中的对象了。有了Statement,我们就可以执行SQL语句。而prepareStatement方法后面会介绍,这里只需要知道它的底层就是
connection.createStatement();
这种方式来创建Statement的。 - 有了Statement对象后,通过StatementHandler的query方法来处理SQL并返回结果集。
起始这个方法中步骤2和步骤3是最重要的。但是真正执行SQL的逻辑还是在query方法中。query方法是由StatementHandler接口中的方法。StatementHandler的继承体系如下
- PreparedStatementHandler:处理JDBC中的PreparedStatement对象
- SimplePreparedStatement:处理JDBC中的Statement对象
- CallableStatementHandler:处理存储过程
- RoutingStatementHandler:使用了策略者模式,它最后所有的逻辑都委托给上面三个实现类执行
我们只需要关心PreparedStatementHandler SimplePreparedStatement
这两个实现类即可
PreparedStatementHandler#query和SimplePreparedStatement#query
上文说到真正执行数据库逻辑的方法是StatementHandler接口中的query方法。并且该接口的两个实现类(PreparedStatementHandler和SimplePreparedStatement)分别实现了JDBC操作中的Statement和PreparedStatement执行SQL的操作。它们的代码比较简单。我就一起贴出来了。代码如下
PreparedStatementHandler#query
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
String sql = boundSql.getSql();
statement.execute(sql);
return resultSetHandler.handleResultSets(statement);
}
复制代码
SimplePreparedStatement#query
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
复制代码
可以看到,query方法都执行调用了JDBC的Statement#execute
执行SQL。最后也都是由ResultHandler对象处理的结果集并返回。有木有激动,看到了JDBC被封装的代码,起始看到JDBC就已经到了MyBatis的最底层了!到此,MyBatis终于揭开它神秘的面纱。
但是我们仅仅是看到了JDBC中的Statement的身影,这只是MyBatis的冰山一角。MyBatis中还有很多很多多西都值得我们学习,像前文提到的Statement对象究竟是如何获取的,以及最后的ResultHandler是如何处理结果集对象的,都值得我们研究。但是对于初学者来说。到此已经掌握了MyBatis的大致流程。虽然文章标题是具体流程,但是限于篇幅有限,我就粗略的介绍一下了。
不过不要失望,我会继续更新MyBatis源码系列的文章!
作者:念念清晰
原文链接:https://juejin.cn/post/7196975187992002597