一、Mybatis源码分析之SelectOne和自定义方法区别
学习一下自定义查询方法与Mybatis提供的方法有什么不同
先执行自定义的方法(学习流程的时候学习过,熟悉)
和以前一样这里不做过多叙述,就是通过jdk动态代理返回一个ProductMapper的接口对象。
下面去执行Product product = productMapper.selectById(168903089414213632L);这段代码,看看和上次查询一个list有什么不同,我们进入这个方法org.apache.ibatis.binding.MapperMethod.MethodSignature#MethodSignature
我们看到这个方法上次做查询的list这里有个returnsMany的判断如果是个集合或者isArray则返回true,但是我们是selectOne并且我们的returnType是个对象所以是返回false(这是不一样的地方)
继续debug 进入org.apache.ibatis.binding.MapperMethod#execute
根据判断都是false所以走else去调用selectOne(这里是不是和我们标题联系上了)其实他最终还是原理还是相同的。
进入org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne(java.lang.String, java.lang.Object)
selectOne其实去调用selectList方法,但是他有个数量校验,这个错我想大家都见过“selectOne but found n(n>1)”
继续往下走进入:org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds)
这里就是上篇查询list<T>流程就一样了,查询完成后拿到list,进行判断
符合条件返回。
下面再看原生的selectOne方法的执行流程
进入selectOnne方法
发现和自定义最后相同还是要去走这个selectOne方法并且通过selectList去查询数据然后进入数量的判断。
但是这里有个不同的,他是怎么把参数与sql语句绑定的,进入:org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
进入org.apache.ibatis.mapping.BoundSql#BoundSql的构造方法
将参数赋值给BoundSql的参数对象parameterObject,这样就绑定成功了。下面的执行流程就一样了。
总结
自定义方法: jdk动态代理返回一个ProductMapper的接口对象,就是MapoerProxy的一个实例化对象。通过invoke方法去判断
最后在返回类型去调用selectOne方法。
原生的selectone就少了一个jdk的动态代理,直接调用selectOne方法(最终还是selectList)
================分割线==================
二、Annotation @Select、@Update、@Insert、@Delete
分析Mybatis通过注解的方式去操作数据库
案例:
看看加了注解后他是怎么去构建的,我们把断点打到解析配置的方法中:org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
看代码也能大致猜到他的过程:
1、解析mapper文件
2、mapper文件中有dao层接口路径
3、通过路径解析查询方法与注解(先去构建mapper文件的sql语句,再去寻找注解方式的sql语句)
进入mapperElement方法:
org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement
查看parse方法:org.apache.ibatis.builder.xml.XMLMapperBuilder#parse
进入org.apache.ibatis.builder.xml.XMLMapperBuilder#bindMapperForNamespace方法绑定映射器
进入addMapper方法(org.apache.ibatis.session.Configuration#addMapper)
重点来了:
注解生成器,进入:org.apache.ibatis.builder.annotation.MapperAnnotationBuilder
和我们的注解@select建立联系了,下面继续进入:org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parse方法
进入org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parseStatement方法
通过调用这个org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#getSqlSourceFromAnnotations方法来获取sqlSource对象继续往下走
通过这个org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#buildSqlSourceFromStrings方法获取sql语句并返回
我们看一下返回的sqlSource参数
这就拿到了sql执行语句,后面的创建回话,查询数据和以前源码分析相同
总结:注解执行流程
>org.apache.ibatis.session.SqlSessionFactoryBuilder.build(java.io.InputStream)
>org.apache.ibatis.builder.xml.XMLConfigBuilder
>org.apache.ibatis.builder.xml.XMLConfigBuilder.mapperElement
>org.apache.ibatis.session.Configuration.addMapper
>org.apache.ibatis.binding.MapperRegistry.addMapper
>org.apache.ibatis.binding.MapperRegistry.addMapper
>org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.parseStatement
>org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.getSqlSourceFromAnnotations
>org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.buildSqlSourceFromStrings
>org.apache.ibatis.builder.SqlSourceBuilder.parse
但是我们这里有个疑问,#{id}是怎么替换成?
我们这里去深入研究下;
通过这个方法(org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#buildSqlSourceFromStrings)获取的sql语句,我们看看他是怎么获取的
进入:org.apache.ibatis.scripting.LanguageDriver#createSqlSource(org.apache.ibatis.session.Configuration, java.lang.String, java.lang.Class<?>)
进入RawSqlSource
拿到参数类型,然后去获取sql源,进入:org.apache.ibatis.builder.SqlSourceBuilder#parse
看着这里,这个parser对象获得:
猜想他是应该要将#{}替换成? 做准备。我们继续往下走看这个方法org.apache.ibatis.parsing.GenericTokenParser#parse
这段就是核心代码。
代码太多不好截图,看不到debug流程,不过我加了自己理解注释
{
if (text == null || text.isEmpty()) {
return "";
}
// search open token
//判断该sql语句到指定字符(#{)有多少长度
int start = text.indexOf(openToken, 0);
if (start == -1) {
return text;
}
//将sql语句转化成char[]数组
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null;
while (start > -1) {
if (start > 0 && src[start - 1] == '\\') {
// this open token is escaped. remove the backslash and continue.
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// found open token. let's search close token.
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
//将sql语句从起始位置到#{位置(不包含#{)append到builder中去
builder.append(src, offset, start - offset);
//此时将偏移量从0移到一段sql语句(select * from...... id=#{id})中的#{ 的{ 的位置
offset = start + openToken.length();
//最后一个{在sql字符串中的偏移量是多少
int end = text.indexOf(closeToken, offset);
while (end > -1) {
if (end > offset && src[end - 1] == '\\') {
// this close token is escaped. remove the backslash and continue.
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
} else {
//将....#{id} 中的参数提取出来
expression.append(src, offset, end - offset);
//将偏移量打到末尾
offset = end + closeToken.length();
break;
}
}
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
//将?放入新的sql语句中去
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
通过以上流程我们终于将"#{}"替换成“?”.后面的查询流程就和上一篇的执行流程一样了,不在过多叙述。
至此@Select注解源码分析结束,其实update,delect都差不多。我们可以从加载类MapperAnnotationBuilder看出来。
===================分割线=========================
Mybatis源码分析之执行器原理
每一个SqlSession都会拥有一个Executor对象,这个对象负责增删改查的具体操作,我们可以简单的将它理解为JDBC中Statement的封装版。
我们进入org.apache.ibatis.executor.Executor接口:
如图所示,位于继承体系最顶层的是Executor执行器,它有两个实现类,分别是BaseExecutor
和 CachingExecutor
。
BaseExecutor
是一个抽象类,这种通过抽象的实现接口的方式是适配器设计模式之接口适配
的体现,是Executor的默认实现,实现了大部分Executor接口定义的功能,降低了接口实现的难度。BaseExecutor的子类有三个,分别是SimpleExecutor
、ReuseExecutor
和BatchExecutor
。
SimpleExecutor: 简单执行器,是MyBatis中默认使用的执行器,每执行一次update或select,就开启一个Statement对象,用完就直接关闭Statement对象(可以是Statement或者是PreparedStatment对象)
ReuseExecutor: 可重用执行器,这里的重用指的是重复使用Statement,它会在内部使用一个Map把创建的Statement都缓存起来,每次执行SQL命令的时候,都会去判断是否存在基于该SQL的Statement对象,如果存在Statement对象并且对应的connection还没有关闭的情况下就继续使用之前的Statement对象,并将其缓存起来。因为每一个SqlSession都有一个新的Executor对象,所以我们缓存在ReuseExecutor上的Statement作用域是同一个SqlSession。
BatchExecutor: 批处理执行器,用于将多个SQL一次性输出到数据库。update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理的;BatchExecutor相当于维护了多个桶,每个桶里都装了很多属于自己的SQL,就像苹果蓝里装了很多苹果,番茄蓝里装了很多番茄,最后,再统一倒进仓库。(可以是Statement或PrepareStatement对象)
CachingExecutor
: 缓存执行器,先从缓存中查询结果,如果存在,就返回;如果不存在,再委托给Executor delegate 去数据库中取,delegate可以是上面任何一个执行器,前面也有说到,先从缓存中去查询,如果查询不到那就从数据库查询。
Executor创建过程以及源码分析:其实在mybatis的执行流程中就提到了,创建会话时:
SqlSession session = sessionFactory.openSession();
回去执行一个newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)方法
通常默认是SImpleExcutor执行器,如果想要更改的话有两种方式:1、在mybatis-config.xml的配置文件中选择好
2、另一种就是创建会话时进行设定
我们看看第二种代码是如何执行(其实也是大同小异,只不过带了一个执行器类型的参数)
这是我们看到执行器的类型是BATCH,继续往下走,看看他如何获取BatchExcutor执行器的,进入org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)
我们发现不管是Simple执行器还是Batch执行器实例化都是走BaseExecutor方法
我们发现一个当他在执行doquery时,他会去调用Batch执行器的方法
我们看看这个BatchExecutor的方法;
public class BatchExecutor extends BaseExecutor {
public static final int BATCH_UPDATE_RETURN_VALUE = Integer.MIN_VALUE + 1002;
/* Statement链表**/
private final List<Statement> statementList = new ArrayList<Statement>();
/* batch结果链表**/
private final List<BatchResult> batchResultList = new ArrayList<BatchResult>();
private String currentSql;
private MappedStatement currentStatement;
public BatchExecutor(Configuration configuration, Transaction transaction) {
super(configuration, transaction);
}
@Override
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
//获得配置信息
final Configuration configuration = ms.getConfiguration();
//获得StatementHandler
final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
//获得Sql语句
final BoundSql boundSql = handler.getBoundSql();
final String sql = boundSql.getSql();
final Statement stmt;
//如果sql语句等于当前sql MappedStatement 等于当前Map碰到Statement
if (sql.equals(currentSql) && ms.equals(currentStatement)) {
int last = statementList.size() - 1;
//获得最后一个
stmt = statementList.get(last);
applyTransactionTimeout(stmt);
handler.parameterize(stmt);//fix Issues 322
//有相同的MappedStatement和参数
BatchResult batchResult = batchResultList.get(last);
batchResult.addParameterObject(parameterObject);
} else {
//如果不存在就创建一个批处理操作
Connection connection = getConnection(ms.getStatementLog());
stmt = handler.prepare(connection, transaction.getTimeout());
//添加批量处理操作
handler.parameterize(stmt); //fix Issues 322
currentSql = sql;
currentStatement = ms;
statementList.add(stmt);
batchResultList.add(new BatchResult(ms, sql, parameterObject));
}
// handler.parameterize(stmt);
//最终是调用jdbc的批处理操作
handler.batch(stmt);
return BATCH_UPDATE_RETURN_VALUE;
}
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
throws SQLException {
Statement stmt = null;
try {
flushStatements();
//获得配置信息
Configuration configuration = ms.getConfiguration();
//获得StatementHandler
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql);
Connection connection = getConnection(ms.getStatementLog());
stmt = handler.prepare(connection, transaction.getTimeout());
//获得Statement
handler.parameterize(stmt);
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
@Override
protected <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException {
flushStatements();
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, null, boundSql);
Connection connection = getConnection(ms.getStatementLog());
Statement stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return handler.<E>queryCursor(stmt);
}
/* 刷新Statement,记录执行次数*/
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
try {
List<BatchResult> results = new ArrayList<BatchResult>();
if (isRollback) {
return Collections.emptyList();
}
for (int i = 0, n = statementList.size(); i < n; i++) {
Statement stmt = statementList.get(i);
applyTransactionTimeout(stmt);
BatchResult batchResult = batchResultList.get(i);
try {
batchResult.setUpdateCounts(stmt.executeBatch());
MappedStatement ms = batchResult.getMappedStatement();
List<Object> parameterObjects = batchResult.getParameterObjects();
KeyGenerator keyGenerator = ms.getKeyGenerator();
if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
} else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141
for (Object parameter : parameterObjects) {
keyGenerator.processAfter(this, ms, stmt, parameter);
}
}
// Close statement to close cursor #1109
closeStatement(stmt);
} catch (BatchUpdateException e) {
StringBuilder message = new StringBuilder();
message.append(batchResult.getMappedStatement().getId())
.append(" (batch index #")
.append(i + 1)
.append(")")
.append(" failed.");
if (i > 0) {
message.append(" ")
.append(i)
.append(" prior sub executor(s) completed successfully, but will be rolled back.");
}
throw new BatchExecutorException(message.toString(), e, results, batchResult);
}
//记录操作
results.add(batchResult);
}
return results;
} finally {
for (Statement stmt : statementList) {
closeStatement(stmt);
}
currentSql = null;
statementList.clear();
batchResultList.clear();
}
}
}
正如概念所说,BatchExecutor执行器就是在update时伪执行,他是等到doQuery方法一起执行。
我们可以看一下对比:
执行mybatis的update语句:
发现BatchExecutor的update方法执行成功后不会返回1,这个需要这注意。在看看数据库有没有变化。
这也验证了他的概念当你使用BatchExecutor方法,一旦执行update类方法,如果没有执行查询方法,那么你执行的sql是不是同步到数据库的。我们再次执行update方法后并执行select方法。看看结果。
更改完成。继续学习,坚持。
=========================结束========================
下一篇:Mybatis的插件与缓存源码解析