源码学习之MyBatis的底层查询原理

2022-12-16 0 352

原副标题:源标识符自学之MyBatis的下层查阅基本原理

编者按

责任编辑透过MyBatis两个旧版的bug(3.4.5以后的版)侧发力,预测MyBatis的一场完备的查阅业务流程,从命令行的导出到两个查阅的完备继续执行操作过程详尽阐释MyBatis的一场查阅业务流程,透过责任编辑能详尽介绍MyBatis的一场查阅操作过程。在平常的标识符撰写中,辨认出了MyBatis两个旧版的bug(3.4.5以后的版),虽然那时许多工程工程建设中的版都是高于3.4.5的,因而在这儿用两个单纯的范例Cadours难题,因而从源标识符视角预测MyBatis一场查阅的业务流程,让我们介绍MyBatis的查阅基本原理

01

难题现像

在去年的灵巧项目组工程建设中,我透过Suite开伞器同时实现了全屏智能化程序标识符。Juint除Suite开伞器除了什么样开伞器呢?继而我的Runnerexplore已经开始了!

1.1 情景难题Cadours

如下表所示图右图,在实例Mapper中,上面提供更多了两个方式queryStudents,从student表中查阅出合乎查阅前提的统计数据,入参能为student_name或是student_name的子集,实例中模块只传至的是studentName的List子集

List<String> studentNames =newLinkedList<>; studentNames.add(“lct”); studentNames.add(“lct2”); condition.setStudentNames(studentNames); <selectid=“queryStudents”parameterType=“mybatis.StudentCondition”resultMap=“resultMap”>

select* fromstudent<where><iftest=“studentNames != null and studentNames.size > 0 “>AND student_name IN<foreachcollection=“studentNames”item=“studentName”open=“(“separator=“,”close=“)”>#{studentName, jdbcType=VARCHAR}</foreach></if>

<iftest=“studentName != null and studentName != “>AND student_name =#{studentName, jdbcType=VARCHAR}</if></where></select>

期望运行的结果是

select* fromstudent WHEREstudent_nameIN( lct, lct2)

但是实际上运行的结果是

==> Preparing: select * from student WHERE student_name IN ( ? , ? ) AND student_name = ?

==> Parameters: lct(String), lct2(String), lct2(String)

<== Columns: id, student_name, age

<== Row: 2, lct2, 2

<== Total: 1

透过运行结果能看到,没有给student_name单独赋值,但是经过MyBatis导出以后,单独给student_name赋值了两个值,能推断出MyBatis在导出SQL并对变量赋值的时候是有难题的,初步猜测是foreach循环中的变量的值带到了foreach外边,导致SQL导出出现异常,上面透过源标识符进行预测验证

02

MyBatis查阅基本原理

,透过事件导出引擎导出用户自定义事件并完成事件的绑定,完成导出赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。

2.1 MyBatis架构

2.1.1 架构图

先单纯来看看MyBatis整体上的架构模型,从整体上看MyBatis主要分为四大模块:

接口层:主要作用就是和统计数据库打交道

统计数据处理层:统计数据处理层能说是MyBatis的核心,它要完成两个功能:

透过传至模块构建动态SQL语句; SQL语句的继续执行以及封装查阅结果集成List<E>

框架支撑层:主要有事务管理、连接池管理、缓存机制和SQL语句的配置方式

引导层:引导层是配置和启动MyBatis 配置信息的方式。MyBatis 提供更多两种方式来引导MyBatis :基于XML命令行的方式和基于Java API 的方式

2.1.2 MyBatis四大对象

贯穿MyBatis整个框架的有四大核心对象,ParameterHandler、ResultSetHandler、StatementHandler和Executor,四大对象贯穿了整个框架的继续执行操作过程,四大对象的主要作用为:

ParameterHandler:设置预编译模块 ResultSetHandler:处理SQL的返回结果集 StatementHandler:处理sql语句预编译,设置模块等相关工作 Executor:MyBatis的开伞器,用于继续执行增删改查操作

2.2 从源标识符阐释MyBatis的一场查阅操作过程

首先给出Cadours难题的标识符以及相应的准备操作过程

2.2.1 统计数据准备

CREATETABLE`student`(`id`bigint(20) NOTNULLAUTO_INCREMENT,`student_name`varchar(255) NULLDEFAULTNULL,`age`int(11) NULLDEFAULTNULL,PRIMARY KEY(`id`) USINGBTREE) ENGINE= InnoDBAUTO_INCREMENT = 1;

— —————————-— Records of student— —————————-INSERTINTO`student`VALUES(1, lct, 1);INSERTINTO`student`VALUES(2, lct2, 2);

2.2.2 标识符准备

1.mapper命令行

<?xml version=”1.0″ encoding=”UTF-8″?><!DOCTYPE mapper PUBLIC “-//mybatis.org//DTD Mapper 3.0//EN” “http://mybatis.org/dtd/mybatis-3-mapper.dtd” >

<mappernamespace=“mybatis.StudentDao”><!– 映射关系 –><resultMapid=“resultMap”type=“mybatis.Student”><idcolumn=“id”property=“id”jdbcType=“BIGINT”/><resultcolumn=“student_name”property=“studentName”jdbcType=“VARCHAR”/><resultcolumn=“age”property=“age”jdbcType=“INTEGER”/>

</resultMap>

<selectid=“queryStudents”parameterType=“mybatis.StudentCondition”resultMap=“resultMap”>

select * from student<where><iftest=“studentNames != null and studentNames.size > 0 “>AND student_name IN<foreachcollection=“studentNames”item=“studentName”open=“(“separator=“,”close=“)”>#{studentName, jdbcType=VARCHAR}</foreach></if>

<iftest=“studentName != null and studentName != “>AND student_name = #{studentName, jdbcType=VARCHAR}</if></where></select>

</mapper>

2.实例标识符

publicstaticvoidmain(String[] args) throws IOException{String resource = “mybatis-config.xml”;InputStream inputStream = Resources.getResourceAsStream(resource);y对象SqlSessionFactory sqlSessionFactory = newSqlSessionFactoryBuilder.build(inputStream);SqlSession sqlSession = sqlSessionFactory.openSession;StudentDao mapper = sqlSession.getMapper(StudentDao.class);StudentCondition condition = newStudentCondition;List<String> studentNames = newLinkedList<>;studentNames.add(“lct”);studentNames.add(“lct2”);condition.setStudentNames(studentNames);//继续执行方式List<Student> students = mapper.queryStudents(condition);}

2.2.3 查阅操作过程预测

1.SqlSessionFactory的构建

先看SqlSessionFactory的对象的创建操作过程

SqlSessionFactory sqlSessionFactory = newSqlSessionFactoryBuilder.build(inputStream); publicSqlSessionFactorybuild(InputStream inputStream){returnbuild(inputStream, null, null);}

调用自身的build方式

源码学习之MyBatis的底层查询原理

图1 build方式自身调用调试图例

在这个方式里会创建两个XMLConfigBuilder的对象,用来导出传至的MyBatis的命令行,然后调用parse方式进行导出

源码学习之MyBatis的底层查询原理

图2 parse导出入参调试图例

在这个方式讲解。这儿能看到导出命令行是从configuration这个节点已经开始的,在MyBatis的命令行中这个节点也是根节点

<?xml version=”1.0″ encoding=”UTF-8″ ?><!DOCTYPE configurationPUBLIC “-//mybatis.org//DTD Config 3.0//EN”“http://mybatis.org/dtd/mybatis-3-config.dtd”><configuration>

<properties><propertyname=“dialect”value=“MYSQL”/><!– SQL方言 –></properties>

源码学习之MyBatis的底层查询原理

图3 导出配置调试图例

<mappers><mapperresource=“mappers/StudentMapper.xml”/></mappers>

进入mapperElement方式

mapperElement(root.evalNode(“mappers”));

源码学习之MyBatis的底层查询原理

图4 mapperElement方式调试图例

看到MyBatis还是透过创建两个XMLMapperBuilder对象来对mappers节点进行导出,在parse方式中

publicvoidparse(){if(!configuration.isResourceLoaded(resource)) {configurationElement(parser.evalNode(“/mapper”));configuration.addLoadedResource(resource);bindMapperForNamespace;}

parsePendingResultMaps;parsePendingCacheRefs;parsePendingStatements;}

透过调用configurationElement方式来导出配置的每两个mapper文件

privatevoidconfigurationElement(XNode context) {try{Stringnamespace= context.getStringAttribute(“namespace”);if(namespace== null|| namespace.equals(“”)) {thrownewBuilderException(“Mappers namespace cannot be empty”);}builderAssistant.setCurrentNamespace(namespace);cacheRefElement(context.evalNode(“cache-ref”));cacheElement(context.evalNode(“cache”));parameterMapElement(context.evalNodes(“/mapper/parameterMap”));resultMapElements(context.evalNodes(“/mapper/resultMap”));sqlElement(context.evalNodes(“/mapper/sql”));buildStatementFromContext(context.evalNodes(“select|insert|update|delete”));} catch(Exception e) {thrownewBuilderException(“Error parsing Mapper XML. Cause: “+ e, e);}}

以导出mapper中的增删改查的标签来看看是如何导出两个mapper文件的

进入buildStatementFromContext方式

privatevoidbuildStatementFromContext(List<XNode> list, String requiredDatabaseId){for(XNode context : list) {final XMLStatementBuilder statementParser = newXMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);try{statementParser.parseStatementNode;} catch(IncompleteElementException e) {configuration.addIncompleteStatement(statementParser);}}}

能看到MyBatis还是透过创建两个XMLStatementBuilder对象来对增删改查节点进行导出,透过调用这个对象的parseStatementNode方式,在这个方式里

图5 parseStatementNode方式调试图例

导出完成以后,透过方式addMappedStatement将所有的配置都添加到两个MappedStatement中去,然后再将mappedstatement添加到configuration中去

builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);

图6 增加导出完成的mapper方式调试图例

能看到两个mappedstatement中包含了两个增删改查标签的详尽信息

图7 mappedstatement对象方式调试图例

而两个configuration就包含了所有的配置信息,其中mapperRegistertry和mappedStatements

图8 config对象方式调试图例

具体的业务流程

图9 SqlSessionFactory对象的构建操作过程

2.SqlSession的创建操作过程

SqlSessionFactory创建完成以后,接下来看看SqlSession的创建操作过程

SqlSessionsqlSession = sqlSessionFactory.openSession;

首先会调用DefaultSqlSessionFactory的openSessionFromDataSource方式

@OverridepublicSqlSession openSession{returnopenSessionFromDataSource(configuration.getDefaultExecutorType,null, false);}

ctionFactory

privateSqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level,booleanautoCommit){Transaction tx = null;try{finalEnvironment environment = configuration.getEnvironment;finalTransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);tx = transactionFactory.newTransaction(environment.getDataSource, level, autoCommit);finalExecutor executor = configuration.newExecutor(tx, execType);returnnewDefaultSqlSession(configuration, executor, autoCommit);} catch(Exception e) {closeTransaction(tx); // may have fetched a connection so lets call closethrowExceptionFactory.wrapException(“Error opening session. Cause: “+ e, e);} finally{ErrorContext.instance.reset;}}

事务创建完成以后已经开始创建Executor对象,Executor对象的创建是根据 executorType创建的,默认是SIMPLE类型的,没有配置的情况下创建了SimpleExecutor,如果开启二级缓存的话,则会创建CachingExecutor

publicExecutor newExecutor(Transaction transaction, ExecutorType executorType){executorType = executorType == null? defaultExecutorType : executorType;executorType = executorType == null? ExecutorType.SIMPLE : executorType;Executor executor;if(ExecutorType.BATCH == executorType) {executor = newBatchExecutor(this, transaction);} elseif(ExecutorType.REUSE == executorType) {executor = newReuseExecutor(this, transaction);} else{executor = newSimpleExecutor(this, transaction);}if(cacheEnabled) {executor = newCachingExecutor(executor);}executor = (Executor) interceptorChain.pluginAll(executor);returnexecutor;}

创建executor以后,会继续执行executor = (Executor) interceptorChain.pluginAll(executor)方式,这个方式对应的含义是使用每两个拦截器包装并返回executor,最后调用DefaultSqlSession方式创建SqlSession

图10 SqlSession对象的创建操作过程

StudentDaomapper = sqlSession.getMapper(StudentDao.class);

在第一步中知道所有的mapper都放在MapperRegistry这个对象中,因而透过调用

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {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);}}

在MyB

public class MapperProxyFactory<T> {

private final Class<T> mapperInterface;private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>;

public MapperProxyFactory(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;}

public Class<T> getMapperInterface {return mapperInterface;}

public Map<Method, MapperMethod> getMethodCache {return methodCache;}

@SuppressWarnings(“unchecked”)protected T newInstance(MapperProxy<T> mapperProxy) {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);}

}

4.查阅操作过程

调用具体的方式

//继续执行方式List<Student> students = mapper.queryStudents(condition);

首先会调用org.apache.ibatis.binding.MapperProxy#invoke的方式,在这个方式中,会调用org.apache.ibatis.binding.MapperMethod#execute

public Object execute(SqlSession sqlSession, Object[] args) {Object result;switch (command.getType) {case INSERT: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.insert(command.getName, param));break;}case UPDATE: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.update(command.getName, param));break;}case DELETE: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.delete(command.getName, param));break;}case SELECT:if (method.returnsVoid && method.hasResultHandler) {executeWithResultHandler(sqlSession, args);result = null;} else if (method.returnsMany) {result = executeForMany(sqlSession, args);} else if (method.returnsMap) {result = executeForMap(sqlSession, args);} else if (method.returnsCursor) {result = executeForCursor(sqlSession, args);} else {Object param = method.convertArgsToSqlCommandParam(args);result = sqlSession.selectOne(command.getName, param);}break;case FLUSH:result = sqlSession.flushStatements;break;default:throw new BindingException(“Unknown execution method for: ” + command.getName);}if (result == null && method.getReturnType.isPrimitive && !method.returnsVoid) {throw new BindingException(“Mapper method ” + command.getName + ” attempted to return null from a method with a primitive return type (” + method.getReturnType + “).”);}return result;}

首先根据SQL的类型增删改查决定继续执行哪个方式,在此继续执行的是SELECT方式,在SELECT中根据方式的返回值类型决定继续执行哪个方式,能看到在select中没有selectone单独方式,都是透过selectList方式,透过调用org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Obje

@Overridepublic <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {try {MappedStatement ms = configuration.getMappedStatement(statement);return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);} catch (Exception e) {throw ExceptionFactory.wrapException(“Error querying database. Cause: ” + e, e);} finally {ErrorContext.instance.reset;}}

ache.ibatis.executor.CachingExecutor#query方式

图12 query方式调试图示

在这个方式中,首先对SQL进行导出根据入参和原始SQL,对SQL进行拼接

图13 SQL拼接操作过程标识符图示

调用MapperedStatement里的getBoundSql最终导出出来的SQL为

图14 SQL拼接操作过程结果图示

接下来调用org.apache.ibatis.parsing.GenericTokenParser#parse对导出出来的SQL进行导出

图15 SQL导出操作过程图示

最终导出的结果为

图16 SQL导出结果图示

最后会调用SimpleExecutor中的doQuery方式

图17 SQL处理结果图示

查阅的主要业务流程为

图18 查阅业务流程处理图示

5.查阅业务流程总结

总结整个查阅业务流程如下表所示

图19 查阅业务流程抽象

2.3 情景难题原因及解决方案

2.3.1 个人排查

这个问bug出现的地方在于绑定SQL模块的时候再源标识符中位置为

@Overridepublic <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameter);CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);return query(ms, parameter, rowBounds, resultHandler, key, boundSql);}

虽然所写的SQL是两个动态绑定模块的SQL,因而最终会走到org.apache.ibatis.ing.xmltags.DynamicSqlSource#getBoundSql这个方式中去

publicBoundSql getBoundSql(ObjectparameterObject) {BoundSql boundSql = sqlSource.getBoundSql(parameterObject);List<ParameterMapping> parameterMappings = boundSql.getParameterMappings;if(parameterMappings ==null|| parameterMappings.isEmpty) {boundSql = newBoundSql(configuration, boundSql.getSql, parameterMap.getParameterMappings, parameterObject);}

// check for nested result maps in parameter mappings (issue #30)for(ParameterMapping pm : boundSql.getParameterMappings) {StringrmId = pm.getResultMapId;if(rmId != null) {ResultMap rm = configuration.getResultMap(rmId);if(rm != null) {hasNestedResultMaps |= rm.hasNestedResultMaps;}}}

returnboundSql;}

在这个方式中,会调用 rootSqlNode.apply(context)方式,虽然这个标签是两个foreach标签,因而这个apply方式会调用到org.apache.ibatis.ing.xmltags.ForEachSqlNode#apply这个方式中去

@Overridepublic boolean apply(DynamicContext context) {Map<String, Object> bindings = context.getBindings;final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);if (!iterable.iterator.hasNext) {return true;}boolean first = true;applyOpen(context);int i = 0;for (Object o : iterable) {DynamicContext oldContext = context;if (first) {context = new PrefixedContext(context, “”);} else if (separator != null) {context = new PrefixedContext(context, separator);} else {context = new PrefixedContext(context, “”);}int uniqueNumber = context.getUniqueNumber;// Issue #709 if (o instanceof Map.Entry) {@SuppressWarnings(“unchecked”) Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;applyIndex(context, mapEntry.getKey, uniqueNumber);applyItem(context, mapEntry.getValue, uniqueNumber);} else {applyIndex(context, i, uniqueNumber);applyItem(context, o, uniqueNumber);}contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));if (first) {first = !((PrefixedContext) context).isPrefixApplied;}context = oldContext;i++;}applyClose(context);return true;}

当调用appItm方式的时候将模块进行绑定,模块的变量难题都会存在bindings这个模块中区

private void applyItem(DynamicContext context, Object o, int i) {if (item != null) {context.bind(item, o);context.bind(itemizeItem(item, i), o);}}

进行绑定模块的时候,绑定完成foreach的方式的时候,能看到bindings中不止绑定了foreach中的两个模块还额外有两个模块名字studentName->lct2,也就是说最后两个模块也是会出那时bindings这个模块中的,

private void applyItem(DynamicContext context, Object o, int i) {if (item != null) {context.bind(item, o);context.bind(itemizeItem(item, i), o);}}

图20 模块绑定操作过程

最后判定

org.apache.ibatis.ing.xmltags.IfSqlNode#apply

@Overridepublic boolean apply(DynamicContext context) {if (evaluator.evaluateBoolean(test, context.getBindings)) {contents.apply(context);return true;}return false;}

能看到在调用evaluateBoolean方式的时候会把context.getBindings就是前边提到的bindings模块传至进去,因为那时这个模块中有两个studentName,因而在使用Ognl表达式的时候,判定为这个if标签是有值的因而将这个标签进行介绍析

图21 单个模块绑定操作过程

最终绑定的结果为

图22 全部模块绑定操作过程

因而这个地方绑定模块的地方是有难题的,至此找出了难题的所在。

2.3.2 官方解释

翻阅MyBatis官方文档进行求证,辨认出在3.4.5版发行中bug fixes中有这样一句

图23 此难题官方修复github记录

修复了foreach版中对于全局变量context的修改的bug

issue地址为https://github.com/mybatis/mybatis-3/pull/966

修复方案为https://github.com/mybatis/mybatis-3/pull/966/commits/84513f915a9dcb97fc1d602e0c06e11a1eef4d6a

能看到官方给出的修改方案,重新定义了两个对象,分别存储全局变量和局部变量,这样就会解决foreach会改变全局变量的难题。

图24 此难题官方修复标识符实例

2.3.3 修复方案

升级MyBatis版至3.4.5以上 如果保持版本不变的话,在foreach中定义的变量名不要和外部的一致

03

源标识符阅读操作过程总结

视图树的结构,转换完成后将透过表达式引擎导出表达式并取得正确的值,透过事件导出引擎导出用户自定义事件并完成事件的绑定,完成导出赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。

MyBatis源标识符的目录是比较清晰的,基本上每个相同功能的模块都在一起,但是如果直接去阅读源标识符的话,可能还是有一定的难度,没法理解它的运行操作过程,本次透过两个单纯的查阅业务流程从头到尾跟下来,能看到MyBatis的设计以及处理业务流程,例如其中用到的设计模式:

图25 MyBatis标识符结构图

组合模式:如ChooseSqlNode,IfSqlNode等 模板方式模式:例如BaseExecutor和SimpleExecutor,除了BaseTypeHandler和所有的子类例如IntegerTypeHandler Builder模式:例如 SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder 工厂模式:例如SqlSessionFactory、ObjectFactory、MapperProxyFactory 代理模式:MyBatis同时实现的核心,比如MapperProxy、ConnectionLogger

04

文档参考

https://mybatis.org/mybatis-3/zh/index.html

END

从商业和开源中找到平衡

这儿有最新开源资讯、软件更新、技术干货等内容

点这儿 ↓↓↓ 记得 关注✔ 标星⭐ 哦~

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务