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

导读

本文通过MyBatis一个低版本的bug(3.4.5之前的版本)入手,分析MyBatis的一次完整的查询流程,从配置文件的解析到一个查询的完整执行过程详细解读MyBatis的一次查询流程,通过本文可以详细了解MyBatis的一次查询过程。在平时的代码编写中,发现了MyBatis一个低版本的bug(3.4.5之前的版本),由于现在很多工程中的版本都是低于3.4.5的,因此在这里用一个简单的例子复现问题,并且从源码角度分析MyBatis一次查询的流程,让大家了解MyBatis的查询原理。

1 问题现象

1.1 场景问题复现

如下图所示,在示例Mapper中,下面提供了一个方法queryStudents,从student表中查询出符合查询条件的数据,入参可以为student_name或者student_name的集合,示例中参数只传入的是studentName的List集合

1 List<String> studentNames = new LinkedList<>(); 2 studentNames.add("lct"); 3 studentNames.add("lct2"); 4 condition.setStudentNames(studentNames);
1 <select id="queryStudents" parameterType="mybatis.StudentCondition" resultMap="resultMap"> 2 3 4 select * from student 5 <where> 6 <if test="studentNames != null and studentNames.size > 0 "> 7 AND student_name IN 8 <foreach collection="studentNames" item="studentName" open="(" separator="," close=")"> 9 #{studentName, jdbcType=VARCHAR} 10 </foreach> 11 </if> 12 13 14 <if test="studentName != null and studentName != '' "> 15 AND student_name = #{studentName, jdbcType=VARCHAR} 16 </if> 17 </where> 18 </select>

期望运行的结果是

select * from student WHERE student_name IN ( '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解析出现异常,下面通过源码进行分析验证

2 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的一次查询过程

首先给出复现问题的代码以及相应的准备过程

2.2.1 数据准备

1CREATE TABLE `student` ( 2 `id` bigint(20) NOT NULL AUTO_INCREMENT, 3 `student_name` varchar(255) NULL DEFAULT NULL, 4 `age` int(11) NULL DEFAULT NULL, 5 PRIMARY KEY (`id`) USING BTREE 6) ENGINE = InnoDB AUTO_INCREMENT = 1; 7 8 9-- ---------------------------- 10-- Records of student 11-- ---------------------------- 12INSERT INTO `student` VALUES (1, 'lct', 1); 13INSERT INTO `student` VALUES (2, 'lct2', 2);

2.2.2 代码准备

1.mapper配置文件

1<?xml version="1.0" encoding="UTF-8"?> 2<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > 3 4 5<mapper namespace="mybatis.StudentDao"> 6 <!-- 映射关系 --> 7 <resultMap id="resultMap" type="mybatis.Student"> 8 <id column="id" property="id" jdbcType="BIGINT" /> 9 <result column="student_name" property="studentName" jdbcType="VARCHAR" /> 10 <result column="age" property="age" jdbcType="INTEGER" /> 11 12 13 </resultMap> 14 15 16 <select id="queryStudents" parameterType="mybatis.StudentCondition" resultMap="resultMap"> 17 18 19 select * from student 20 <where> 21 <if test="studentNames != null and studentNames.size > 0 "> 22 AND student_name IN 23 <foreach collection="studentNames" item="studentName" open="(" separator="," close=")"> 24 #{studentName, jdbcType=VARCHAR} 25 </foreach> 26 </if> 27 28 29 <if test="studentName != null and studentName != '' "> 30 AND student_name = #{studentName, jdbcType=VARCHAR} 31 </if> 32 </where> 33 </select> 34 35 36</mapper>

2.示例代码

1public static void main(String[] args) throws IOException { 2 String resource = "mybatis-config.xml"; 3 InputStream inputStream = Resources.getResourceAsStream(resource); 4 //1.获取SqlSessionFactory对象 5 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 6 //2.获取对象 7 SqlSession sqlSession = sqlSessionFactory.openSession(); 8 //3.获取接口的代理类对象 9 StudentDao mapper = sqlSession.getMapper(StudentDao.class); 10 StudentCondition condition = new StudentCondition(); 11 List<String> studentNames = new LinkedList<>(); 12 studentNames.add("lct"); 13 studentNames.add("lct2"); 14 condition.setStudentNames(studentNames); 15 //执行方法 16 List<Student> students = mapper.queryStudents(condition); 17 }

2.2.3 查询过程分析

1.SqlSessionFactory的构建

先看SqlSessionFactory的对象的创建过程

1//1.获取SqlSessionFactory对象 2SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

代码中首先通过调用SqlSessionFactoryBuilder中的build方法来获取对象,进入build方法

1 public SqlSessionFactory build(InputStream inputStream) { 2 return build(inputStream, null, null); 3 }

调用自身的build方法

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

在这个方法里会创建一个XMLConfigBuilder的对象,用来解析传入的MyBatis的配置文件,然后调用parse方法进行解析

图2 parse解析入参调试图例

在这个方法中,会从MyBatis的配置文件的根目录中获取xml的内容,其中parser这个对象是一个XPathParser的对象,这个是专门用来解析xml文件的,具体怎么从xml文件中获取到各个节点这里不再进行讲解。这里可以看到解析配置文件是从configuration这个节点开始的,在MyBatis的配置文件中这个节点也是根节点

1<?xml version="1.0" encoding="UTF-8" ?> 2<!DOCTYPE configuration 3 PUBLIC "-//mybatis.org//DTD Config 3.0//EN" 4 "http://mybatis.org/dtd/mybatis-3-config.dtd"> 5<configuration> 6 7 8 <properties> 9 <property name="dialect" value="MYSQL" /> <!-- SQL方言 --> 10 </properties>

然后将解析好的xml文件传入parseConfiguration方法中,在这个方法中会获取在配置文件中的各个节点的配置

图3 解析配置调试图例

以获取mappers节点的配置来看具体的解析过程

1 <mappers> 2 <mapper resource="mappers/StudentMapper.xml"/> 3 </mappers>

进入mapperElement方法

mapperElement(root.evalNode("mappers"));

图4 mapperElement方法调试图例

看到MyBatis还是通过创建一个XMLMapperBuilder对象来对mappers节点进行解析,在parse方法中

1public void parse() { 2 if (!configuration.isResourceLoaded(resource)) { 3 configurationElement(parser.evalNode("/mapper")); 4 configuration.addLoadedResource(resource); 5 bindMapperForNamespace(); 6 } 7 8 9 parsePendingResultMaps(); 10 parsePendingCacheRefs(); 11 parsePendingStatements(); 12}

通过调用configurationElement方法来解析配置的每一个mapper文件

1private void configurationElement(XNode context) { 2 try { 3 String namespace = context.getStringAttribute("namespace"); 4 if (namespace == null || namespace.equals("")) { 5 throw new BuilderException("Mapper's namespace cannot be empty"); 6 } 7 builderAssistant.setCurrentNamespace(namespace); 8 cacheRefElement(context.evalNode("cache-ref")); 9 cacheElement(context.evalNode("cache")); 10 parameterMapElement(context.evalNodes("/mapper/parameterMap")); 11 resultMapElements(context.evalNodes("/mapper/resultMap")); 12 sqlElement(context.evalNodes("/mapper/sql")); 13 buildStatementFromContext(context.evalNodes("select|insert|update|delete")); 14 } catch (Exception e) { 15 throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e); 16 } 17}

以解析mapper中的增删改查的标签来看看是如何解析一个mapper文件的

进入buildStatementFromContext方法

1private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { 2 for (XNode context : list) { 3 final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); 4 try { 5 statementParser.parseStatementNode(); 6 } catch (IncompleteElementException e) { 7 configuration.addIncompleteStatement(statementParser); 8 } 9 } 10}

可以看到MyBatis还是通过创建一个XMLStatementBuilder对象来对增删改查节点进行解析,通过调用这个对象的parseStatementNode方法,在这个方法里会获取到配置在这个标签下的所有配置信息,然后进行设置

图5 parseStatementNode方法调试图例

解析完成以后,通过方法addMappedStatement将所有的配置都添加到一个MappedStatement中去,然后再将mappedstatement添加到configuration中去

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

可以看到一个mappedstatement中包含了一个增删改查标签的详细信息

图7 mappedstatement对象方法调试图例

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

图8 config对象方法调试图例

具体的流程

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

2.SqlSession的创建过程

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

SqlSession sqlSession = sqlSessionFactory.openSession();

首先会调用DefaultSqlSessionFactory的openSessionFromDataSource方法

1@Override 2public SqlSession openSession() { 3 return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); 4}

在这个方法中,首先会从configuration中获取DataSource等属性组成对象Environment,利用Environment内的属性构建一个事务对象TransactionFactory

1private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { 2 Transaction tx = null; 3 try { 4 final Environment environment = configuration.getEnvironment(); 5 final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); 6 tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); 7 final Executor executor = configuration.newExecutor(tx, execType); 8 return new DefaultSqlSession(configuration, executor, autoCommit); 9 } catch (Exception e) { 10 closeTransaction(tx); // may have fetched a connection so lets call close() 11 throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); 12 } finally { 13 ErrorContext.instance().reset(); 14 } 15}

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

1public Executor newExecutor(Transaction transaction, ExecutorType executorType) { 2 executorType = executorType == null ? defaultExecutorType : executorType; 3 executorType = executorType == null ? ExecutorType.SIMPLE : executorType; 4 Executor executor; 5 if (ExecutorType.BATCH == executorType) { 6 executor = new BatchExecutor(this, transaction); 7 } else if (ExecutorType.REUSE == executorType) { 8 executor = new ReuseExecutor(this, transaction); 9 } else { 10 executor = new SimpleExecutor(this, transaction); 11 } 12 if (cacheEnabled) { 13 executor = new CachingExecutor(executor); 14 } 15 executor = (Executor) interceptorChain.pluginAll(executor); 16 return executor; 17}

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

图10 SqlSession对象的创建过程

3.Mapper的获取过程

有了SqlSessionFactory和SqlSession以后,就需要获取对应的Mapper,并执行mapper中的方法

StudentDao mapper = sqlSession.getMapper(StudentDao.class);

在第一步中知道所有的mapper都放在MapperRegistry这个对象中,因此通过调用
org.apache.ibatis.binding.MapperRegistry#getMapper方法来获取对应的mapper

1public <T> T getMapper(Class<T> type, SqlSession sqlSession) { 2 final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); 3 if (mapperProxyFactory == null) { 4 throw new BindingException("Type " + type + " is not known to the MapperRegistry."); 5 } 6 try { 7 return mapperProxyFactory.newInstance(sqlSession); 8 } catch (Exception e) { 9 throw new BindingException("Error getting mapper instance. Cause: " + e, e); 10 } 11}

在MyBatis中,所有的mapper对应的都是一个代理类,获取到mapper对应的代理类以后执行newInstance方法,获取到对应的实例,这样就可以通过这个实例进行方法的调用

1public class MapperProxyFactory<T> { 2 3 4 private final Class<T> mapperInterface; 5 private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>(); 6 7 8 public MapperProxyFactory(Class<T> mapperInterface) { 9 this.mapperInterface = mapperInterface; 10 } 11 12 13 public Class<T> getMapperInterface() { 14 return mapperInterface; 15 } 16 17 18 public Map<Method, MapperMethod> getMethodCache() { 19 return methodCache; 20 } 21 22 23 @SuppressWarnings("unchecked") 24 protected T newInstance(MapperProxy<T> mapperProxy) { 25 return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); 26 } 27 28 29 public T newInstance(SqlSession sqlSession) { 30 final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); 31 return newInstance(mapperProxy); 32 } 33 34 35}

获取mapper的流程为

图11 Mapper的获取过程

4.查询过程

获取到mapper以后,就可以调用具体的方法

1//执行方法 2List<Student> students = mapper.queryStudents(condition);

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

1public Object execute(SqlSession sqlSession, Object[] args) { 2 Object result; 3 switch (command.getType()) { 4 case INSERT: { 5 Object param = method.convertArgsToSqlCommandParam(args); 6 result = rowCountResult(sqlSession.insert(command.getName(), param)); 7 break; 8 } 9 case UPDATE: { 10 Object param = method.convertArgsToSqlCommandParam(args); 11 result = rowCountResult(sqlSession.update(command.getName(), param)); 12 break; 13 } 14 case DELETE: { 15 Object param = method.convertArgsToSqlCommandParam(args); 16 result = rowCountResult(sqlSession.delete(command.getName(), param)); 17 break; 18 } 19 case SELECT: 20 if (method.returnsVoid() && method.hasResultHandler()) { 21 executeWithResultHandler(sqlSession, args); 22 result = null; 23 } else if (method.returnsMany()) { 24 result = executeForMany(sqlSession, args); 25 } else if (method.returnsMap()) { 26 result = executeForMap(sqlSession, args); 27 } else if (method.returnsCursor()) { 28 result = executeForCursor(sqlSession, args); 29 } else { 30 Object param = method.convertArgsToSqlCommandParam(args); 31 result = sqlSession.selectOne(command.getName(), param); 32 } 33 break; 34 case FLUSH: 35 result = sqlSession.flushStatements(); 36 break; 37 default: 38 throw new BindingException("Unknown execution method for: " + command.getName()); 39 } 40 if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { 41 throw new BindingException("Mapper method '" + command.getName() 42 + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); 43 } 44 return result; 45}

首先根据SQL的类型增删改查决定执行哪个方法,在此执行的是SELECT方法,在SELECT中根据方法的返回值类型决定执行哪个方法,可以看到在select中没有selectone单独方法,都是通过selectList方法,通过调用
org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object)方法来获取到数据

1@Override 2public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { 3 try { 4 MappedStatement ms = configuration.getMappedStatement(statement); 5 return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); 6 } catch (Exception e) { 7 throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); 8 } finally { 9 ErrorContext.instance().reset(); 10 } 11}

在selectList中,首先从configuration对象中获取MappedStatement,在statement中包含了Mapper的相关信息,然后调用
org.apache.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方法,在这个方法中,会获取StatementHandler,然后调用
org.apache.ibatis.executor.statement.PreparedStatementHandler#parameterize这个方法进行参数和SQL的处理,最后调用statement的execute方法获取到结果集,然后 利用resultHandler对结进行处理

图17 SQL处理结果图示

查询的主要流程为

图18 查询流程处理图示

5.查询流程总结

总结整个查询流程如下

图19 查询流程抽象

2.3 场景问题原因及解决方案

2.3.1 个人排查

这个问bug出现的地方在于绑定SQL参数的时候再源码中位置为

1 @Override 2 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { 3 BoundSql boundSql = ms.getBoundSql(parameter); 4 CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); 5 return query(ms, parameter, rowBounds, resultHandler, key, boundSql); 6}

由于所写的SQL是一个动态绑定参数的SQL,因此最终会走到
org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql这个方法中去

1public BoundSql getBoundSql(Object parameterObject) { 2 BoundSql boundSql = sqlSource.getBoundSql(parameterObject); 3 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); 4 if (parameterMappings == null || parameterMappings.isEmpty()) { 5 boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject); 6 } 7 8 9 // check for nested result maps in parameter mappings (issue #30) 10 for (ParameterMapping pm : boundSql.getParameterMappings()) { 11 String rmId = pm.getResultMapId(); 12 if (rmId != null) { 13 ResultMap rm = configuration.getResultMap(rmId); 14 if (rm != null) { 15 hasNestedResultMaps |= rm.hasNestedResultMaps(); 16 } 17 } 18 } 19 20 21 return boundSql; 22}

在这个方法中,会调用 rootSqlNode.apply(context)方法,由于这个标签是一个foreach标签,因此这个apply方法会调用到
org.apache.ibatis.scripting.xmltags.ForEachSqlNode#apply这个方法中去

1@Override 2public boolean apply(DynamicContext context) { 3 Map<String, Object> bindings = context.getBindings(); 4 final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings); 5 if (!iterable.iterator().hasNext()) { 6 return true; 7 } 8 boolean first = true; 9 applyOpen(context); 10 int i = 0; 11 for (Object o : iterable) { 12 DynamicContext oldContext = context; 13 if (first) { 14 context = new PrefixedContext(context, ""); 15 } else if (separator != null) { 16 context = new PrefixedContext(context, separator); 17 } else { 18 context = new PrefixedContext(context, ""); 19 } 20 int uniqueNumber = context.getUniqueNumber(); 21 // Issue #709 22 if (o instanceof Map.Entry) { 23 @SuppressWarnings("unchecked") 24 Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o; 25 applyIndex(context, mapEntry.getKey(), uniqueNumber); 26 applyItem(context, mapEntry.getValue(), uniqueNumber); 27 } else { 28 applyIndex(context, i, uniqueNumber); 29 applyItem(context, o, uniqueNumber); 30 } 31 contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber)); 32 if (first) { 33 first = !((PrefixedContext) context).isPrefixApplied(); 34 } 35 context = oldContext; 36 i++; 37 } 38 applyClose(context); 39 return true; 40}

当调用appItm方法的时候将参数进行绑定,参数的变量问题都会存在bindings这个参数中区

1private void applyItem(DynamicContext context, Object o, int i) { 2 if (item != null) { 3 context.bind(item, o); 4 context.bind(itemizeItem(item, i), o); 5 } 6}

进行绑定参数的时候,绑定完成foreach的方法的时候,可以看到bindings中不止绑定了foreach中的两个参数还额外有一个参数名字studentName->lct2,也就是说最后一个参数也是会出现在bindings这个参数中的,

1private void applyItem(DynamicContext context, Object o, int i) { 2 if (item != null) { 3 context.bind(item, o); 4 context.bind(itemizeItem(item, i), o); 5 } 6}

图20 参数绑定过程

最后判定

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

1@Override 2public boolean apply(DynamicContext context) { 3 if (evaluator.evaluateBoolean(test, context.getBindings())) { 4 contents.apply(context); 5 return true; 6 } 7 return false; 8} 9 10

可以看到在调用evaluateBoolean方法的时候会把context.getBindings()就是前边提到的bindings参数传入进去,因为现在这个参数中有一个studentName,因此在使用Ognl表达式的时候,判定为这个if标签是有值的因此将这个标签进行了解析

图21 单个参数绑定过程

最终绑定的结果为

图22 全部参数绑定过程

因此这个地方绑定参数的地方是有问题的,至此找出了问题的所在。

2.3.2 官方解释

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

图23 此问题官方修复github记录 图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中定义的变量名不要和外部的一致

3 源码阅读过程总结

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

图25 MyBatis代码结构图

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

4 文档参考

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

点赞
收藏

评论区

加载中...

相关推荐

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

springboot模糊查询

在学习MyBatis过程中想实现模糊查询,可惜失败了。后来上百度上查了一下,算是解决了。记录一下MyBatis实现模糊查询的几种方式。  数据库表名为test\_student,初始化了几条记录,如图:  !数据库表内容(https://oscimg.oschina.net/oscnet/29429857a651e58b6de1593a923

基于Maven工程下的MyBatis基本使用之数据插入【回填】、修改与删除

MyBatis基本使用声明:基于《基于Maven工程下的MyBatis框架MySQL连接池的数据查询操作》与《基于Maven工程下的MyBatis基本使用之SQL传单/多参、多表关联查询》进一步拓展,相关配置文件、数据文件可阅以上两篇。数据插入<insert,使用<selectKey进行回填自动生成主键值<!需要明确编写获取最新主键的SQL语句<in

Mybatis查询的时候BigDecimal类型的值查询失效的解决办法

最近在使用Mybatis查询的时候,使用了BigDecimal类型的值进行查询,在控制台通过打印的sql发现,查询条件并没有拼接上去,导致查询失败。为了演示还原这个过程,特意写了一个简单的演示项目:比如:我现在查询productprice字段大于0的数据,数据库的数据如下所示:mapper.xml中配置如下:javaselecti

Mabatis中#{}和${}的区别

动态sql是mybatis的主要特性之一,在mapper中定义的参数传到xml中之后,在查询之前mybatis会对其进行动态解析。mybatis为我们提供了两种支持动态sql的语法:{}以及${}。  在下面的语句中,如果username的值为zhangsan,则两种方式无任何区别:selectfr

Mybatis工作原理

引言在mybatis的基础知识中我们已经可以对mybatis的工作方式窥斑见豹(参考:《MyBatis————基础知识》)。本片博客针对Mybatis内部工作原理进行阐述。一、Mybatis工作原理图mybatis原理图如下所示:二、工作原理解析mybatis应用程序通过SqlSessionFactoryBuilder从myb