El principio de consulta subyacente de MyBatis para el aprendizaje del código fuente

Lectura guiada

Este artículo comienza con un error de versión baja de MyBatis (versión anterior a la 3.4.5), analiza un proceso de consulta completo de MyBatis e interpreta un proceso de consulta de MyBatis en detalle desde el análisis del archivo de configuración hasta el proceso completo de ejecución de una consulta. Conoce más sobre el proceso de consulta única de MyBatis. En la escritura de código habitual, se encontró un error en una versión baja de MyBatis (la versión anterior a la 3.4.5), dado que la versión en muchos proyectos es inferior a la 3.4.5, se reproduce aquí con un problema de ejemplo simple, y analizar el proceso de consulta de MyBatis desde la perspectiva del código fuente, para que todos puedan entender el principio de consulta de MyBatis.

 

1 Problema fenómeno

1.1 Reproducción del problema del escenario

Como se muestra en la figura a continuación, en el mapeador de ejemplo, se proporciona un método queryStudents a continuación para consultar los datos que cumplen las condiciones de consulta de la tabla de estudiantes. El parámetro de entrada puede ser nombre_estudiante o una colección de nombre_estudiante. En el ejemplo, el parámetro solo pasa en la Lista de nombre del estudiante.

 List<String> studentNames = new LinkedList<>();
 studentNames.add("lct");
 studentNames.add("lct2");
 condition.setStudentNames(studentNames);

 

  <select id="queryStudents" parameterType="mybatis.StudentCondition" resultMap="resultMap">


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


            <if test="studentName != null and studentName != '' ">
                AND student_name = #{studentName, jdbcType=VARCHAR}
            </if>
        </where>
    </select>

El resultado esperado de la ejecución es

select * from student WHERE student_name IN ( 'lct' , 'lct2' )

Pero el resultado real de correr es

==> Preparando: seleccione * de estudiante DONDE nombre_estudiante IN ( ? , ? ) Y nombre_estudiante = ?

==> Parámetros: lct(Cadena), lct2(Cadena), lct2(Cadena)

<== Columnas: id, nombre_del_estudiante, edad

<== Fila: 2, lct2, 2

<== Total: 1

A partir de los resultados de la ejecución, se puede ver que a nombre_del_estudiante no se le asigna un valor separado, pero después del análisis de MyBatis, se asigna un valor solo a nombre_del_estudiante. Se puede inferir que MyBatis tiene un problema al analizar SQL y asignar valores a variables. La conjetura inicial es el bucle foreach. El valor de la variable en el foreach se lleva al exterior del foreach, lo que da como resultado una excepción en el análisis de SQL.

2 Principio de consulta de MyBatis

2.1 Arquitectura MyBatis

2.1.1 Diagrama de arquitectura

Echemos un breve vistazo al modelo de arquitectura general de MyBatis. En general, MyBatis se divide principalmente en cuatro módulos:

Capa de interfaz : la función principal es tratar con la base de datos.

Capa de procesamiento de datos : Se puede decir que la capa de procesamiento de datos es el núcleo de MyBatis, tiene dos funciones:

  • Cree sentencias SQL dinámicas pasando parámetros;
  • Ejecución de declaraciones SQL y encapsulación de resultados de consultas para integrar List<E>

Capa de soporte del marco : incluye principalmente la gestión de transacciones, la gestión de grupos de conexiones, el mecanismo de almacenamiento en caché y la configuración de sentencias SQL

Capa de arranque : La capa de arranque es la forma de configurar e iniciar la información de configuración de MyBatis. MyBatis proporciona dos formas de guiar a MyBatis: basado en archivos de configuración XML y basado en API Java

2.1.2 Cuatro objetos MyBatis

Hay cuatro objetos principales que se ejecutan en todo el marco de MyBatis, ParameterHandler, ResultSetHandler, StatementHandler y Executor. Los cuatro objetos se ejecutan en el proceso de ejecución de todo el marco. Las funciones principales de los cuatro objetos son:

  • ParameterHandler: establecer parámetros precompilados
  • ResultSetHandler: maneja el conjunto de resultados devuelto de SQL
  • StatementHandler: maneja la precompilación de sentencias sql, la configuración de parámetros y otros trabajos relacionados
  • Ejecutor: El ejecutor de MyBatis, utilizado para realizar operaciones de adición, eliminación, modificación y consulta.

2.2 Interpretar un proceso de consulta de MyBatis a partir del código fuente

Primero dar el código para reproducir el problema y el proceso de preparación correspondiente

2.2.1 Preparación de datos

CREATE TABLE `student`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `student_name` varchar(255) NULL DEFAULT NULL,
  `age` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1;


-- ----------------------------
-- Records of student
-- ----------------------------
INSERT INTO `student` VALUES (1, 'lct', 1);
INSERT INTO `student` VALUES (2, 'lct2', 2);

 

2.2.2 Preparación del código

1.archivo de configuración del mapeador

<?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" >


<mapper namespace="mybatis.StudentDao">
    <!-- 映射关系 -->
    <resultMap id="resultMap" type="mybatis.Student">
        <id column="id" property="id" jdbcType="BIGINT" />
        <result column="student_name" property="studentName" jdbcType="VARCHAR" />
        <result column="age" property="age" jdbcType="INTEGER" />


    </resultMap>


    <select id="queryStudents" parameterType="mybatis.StudentCondition" resultMap="resultMap">


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


            <if test="studentName != null and studentName != '' ">
                AND student_name = #{studentName, jdbcType=VARCHAR}
            </if>
        </where>
    </select>


</mapper>

2. Código de muestra

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

 

2.2.3 Análisis del proceso de consulta

1. Construcción de SqlSessionFactory

Primer vistazo al proceso de creación del objeto SqlSessionFactory

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

El código primero obtiene el objeto llamando al método de compilación en SqlSessionFactoryBuilder e ingresa al método de compilación

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

llamar a su propio método de compilación

Figura 1 El propio método de compilación llama a la leyenda de depuración

En este método, se creará un objeto XMLConfigBuilder para analizar el archivo de configuración entrante de MyBatis, y luego se llamará al método parse para analizar.

Figura 2 Leyenda de depuración de parámetros de entrada de análisis de análisis

En este método, el contenido de xml se obtendrá del directorio raíz del archivo de configuración de MyBatis. El objeto analizador es un objeto XPathParser, que se usa especialmente para analizar archivos xml. ¿Cómo obtener cada nodo del archivo xml? No aquí se da más explicación. Aquí puede ver que el análisis del archivo de configuración comienza desde el nodo de configuración, que también es el nodo raíz en el archivo de configuración de MyBatis.

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


    <properties>
        <property name="dialect" value="MYSQL" />  <!-- SQL方言 -->
    </properties>

Luego, pase el archivo xml analizado al método parseConfiguration, en el que se obtendrá la configuración de cada nodo en el archivo de configuración.

Figura 3 Leyenda de depuración de configuración de análisis

Para obtener la configuración del nodo de mapeadores para ver el proceso de análisis específico

 <mappers>
        <mapper resource="mappers/StudentMapper.xml"/>
    </mappers>

Introduce el método mapperElement

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

Figura 4 Leyenda de depuración del método mapperElement

Ver que MyBatis aún analiza el nodo de mapeadores creando un objeto XMLMapperBuilder, en el método parse

public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();
  }


  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}

Analice cada archivo de mapeador configurado llamando al método configurationElement

private void configurationElement(XNode context) {
  try {
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
      throw new BuilderException("Mapper's 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) {
    throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
  }
}

Echemos un vistazo a cómo analizar un archivo de mapeador analizando las etiquetas CRUD en mapper

Ingrese el método buildStatementFromContext

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

Se puede ver que MyBatis todavía analiza los nodos de adición, eliminación, modificación y consulta mediante la creación de un objeto XMLStatementBuilder. Al llamar al método parseStatementNode de este objeto, toda la información de configuración configurada bajo esta etiqueta se obtendrá en este método y luego se establecerá.

Figura 5 Leyenda de depuración del método parseStatementNode

Una vez que se completa el análisis, agregue todas las configuraciones a un MappedStatement a través del método addMappedStatement, y luego agregue el mappedstatement a la configuración.

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

 

Puede ver que una declaración mapeada contiene los detalles de una etiqueta CRUD

Figura 7 Leyenda de depuración del método del objeto Mappedstatement

Y una configuración contiene toda la información de configuración, incluidos mapperRegistertry y mappedStatements.

Figura 8 Leyenda de depuración del método de objeto de configuración

proceso especifico

Figura 9 El proceso de construcción del objeto SqlSessionFactoryFigura 9 El proceso de construcción del objeto SqlSessionFactory

2. Proceso de creación de SqlSession

Después de crear SqlSessionFactory, echemos un vistazo al proceso de creación de SqlSession.

SqlSession sqlSession = sqlSessionFactory.openSession();

Primero, se llamará al método openSessionFromDataSource de DefaultSqlSessionFactory

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

En este método, las propiedades como DataSource se obtienen primero de la configuración para formar el objeto Environment, y se construye un objeto de transacción TransactionFactory utilizando las propiedades del Environment.

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    final Environment environment = configuration.getEnvironment();
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    final Executor executor = configuration.newExecutor(tx, execType);
    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();
  }
}

Después de crear la transacción, se crea el objeto Executor. La creación del objeto Executor se basa en executorType. El valor predeterminado es el tipo SIMPLE. Si no hay configuración, se crea SimpleExecutor. Si el caché de segundo nivel está habilitado, se creará el CachingExecutor.

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
  executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  Executor executor;
  if (ExecutorType.BATCH == executorType) {
    executor = new BatchExecutor(this, transaction);
  } else if (ExecutorType.REUSE == executorType) {
    executor = new ReuseExecutor(this, transaction);
  } else {
    executor = new SimpleExecutor(this, transaction);
  }
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

Después de crear el ejecutor, se ejecutará el método executor = (Executor)
interceptorChain.pluginAll(executor) El significado correspondiente de este método es usar cada interceptor para envolver y devolver el ejecutor, y finalmente llamar al método DefaultSqlSession para crear un Sesión Sql

Figura 10 El proceso de creación del objeto SqlSession

3. El proceso de adquisición de Mapper

Con SqlSessionFactory y SqlSession, debe obtener el asignador correspondiente y ejecutar los métodos en el asignador.

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

En el primer paso, sabemos que todos los mapeadores se colocan en el objeto MapperRegistry, así
que obtenga el mapeador correspondiente llamando al método org.apache.ibatis.binding.MapperRegistry#getMapper

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);
  }
}

En MyBatis todos los mapeadores corresponden a una clase proxy, luego de obtener la clase proxy correspondiente al mapeador, ejecutar el método newInstance para obtener la instancia correspondiente, para que el método pueda ser llamado a través de esta instancia.

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);
  }


}

El proceso de obtención del mapeador es

Figura 11 El proceso de adquisición de Mapper

4. Proceso de consulta

Después de obtener el mapeador, puede llamar al método específico

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

Primero
, se llamará al método de org.apache.ibatis.binding.MapperProxy#invoke En este método, se llamará a 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;
}

Primero, determine qué método ejecutar según el tipo de SQL, agregue, elimine, modifique y verifique. Aquí, se ejecuta el método SELECT. En SELECT, qué método ejecutar se determina según el tipo de valor de retorno del método. Se puede ver que no hay un método separado para seleccionar uno en select, pero se usa el método selectList, obtenga los datos llamando a
org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang .Objeto) método

@Override
public <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();
  }
}

En selectList, primero obtenga MappedStatement del objeto de configuración, que contiene la información relevante de Mapper en la declaración, y luego llame al
método org.apache.ibatis.executor.CachingExecutor#query()

Figura 12 Diagrama de depuración del método query()

En este método, el SQL primero se analiza y el SQL se empalma de acuerdo con los parámetros de entrada y el SQL original.

Figura 13 Diagrama de código del proceso de empalme de SQL

El SQL finalmente analizado llamando a getBoundSql en MapperedStatement es

Figura 14 Diagrama del resultado del proceso de empalme de SQL

A continuación, llame a
org.apache.ibatis.parsing.GenericTokenParser#parse para analizar el SQL analizado.

Figura 15 Diagrama del proceso de análisis de SQL

El resultado final del análisis es

Figura 16 Diagrama de resultados de análisis de SQL

Finalmente, se llamará al método doQuery en SimpleExecutor, en este método se obtendrá el StatementHandler, luego
se llamará al método org.apache.ibatis.executor.statement.PreparedStatementHandler#parameterize para procesar parámetros y SQL, y finalmente al Se llamará al método de ejecución de la declaración para obtener el conjunto de resultados y luego usar el manejador de resultados para procesar el nudo.

Figura 17 Diagrama de resultados del procesamiento de SQL

El proceso principal de la consulta es

 

Figura 18 Diagrama de procesamiento de flujo de consultas

5. Resumen del proceso de consulta

Todo el proceso de consulta se resume de la siguiente manera

Figura 19 Abstracción del proceso de consulta

2.3 Causas y soluciones del problema del escenario

2.3.1 Investigación personal

El lugar donde aparece este error es que al enlazar parámetros SQL, la ubicación en el código fuente es

 @Override
 public <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);
}

Dado que el SQL escrito es un SQL con parámetros enlazados dinámicamente, eventualmente irá al
método org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql

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


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


  return boundSql;
}

En este método, se llamará al método rootSqlNode.apply(context) Dado que esta etiqueta es una etiqueta foreach, se llamará al
método apply al método org.apache.ibatis.scripting.xmltags.ForEachSqlNode#apply

@Override
public 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;
}

Cuando se llama al método appItm, los parámetros están vinculados y los problemas variables de los parámetros existirán en el área de parámetros de los enlaces.

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

Al vincular parámetros, al vincular el método foreach, puede ver que no solo los dos parámetros en foreach están vinculados en los enlaces, sino también un nombre de parámetro adicional studentName->lct2, lo que significa que el último parámetro también aparecerá en el parámetro de enlaces,

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

 

Figura 20 Proceso de vinculación de parámetros

juicio final

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

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

Se puede ver que cuando se llama al método de evaluación booleana, context.getBindings() es el parámetro de enlaces mencionado anteriormente y se pasa, porque ahora hay un nombre de estudiante en este parámetro, por lo que cuando se usa la expresión Ognl, se determina así si la etiqueta es valiosa, entonces esta etiqueta se analiza

Figura 21 Proceso de vinculación de un solo parámetro

El resultado vinculante final es

Figura 22 Proceso de vinculación de todos los parámetros

Por lo tanto, hay un problema con el parámetro de enlace en este lugar y el problema se ha encontrado hasta ahora.

2.3.2 Explicación oficial

Lea la documentación oficial de MyBatis para la verificación y descubrí que existe una oración de este tipo en las correcciones de errores en el lanzamiento de la versión 3.4.5

Figura 23 Este problema se solucionó oficialmente en el registro de github Figura 23 Este problema se solucionó oficialmente en el registro de github

Se corrigió un error en la modificación del contexto de la variable global en la versión foreach

La dirección del problema es https://github.com/mybatis/mybatis-3/pull/966

La solución es https://github.com/mybatis/mybatis-3/pull/966/commits/84513f915a9dcb97fc1d602e0c06e11a1eef4d6a

 

Puede ver el plan de modificación oficial, redefiniendo un objeto para almacenar variables globales y variables locales respectivamente, lo que resolverá el problema de que foreach cambiará las variables globales.

Figura 24 Un ejemplo del código de reparación oficial para este problema

2.3.3 Plan de reparación

  • Actualice la versión de MyBatis a una superior a la 3.4.5
  • Si la versión permanece sin cambios, el nombre de la variable definida en foreach no debe ser consistente con la externa

3 Resumen del proceso de lectura del código fuente

El directorio del código fuente de MyBatis es relativamente claro, básicamente todos los módulos con la misma función están juntos, pero si lees el código fuente directamente, aún puede ser difícil entender su proceso de ejecución, esta vez a través de un simple proceso de consulta. desde el principio hasta el final, y puedes ver el flujo de diseño y procesamiento de MyBatis, como los patrones de diseño utilizados en él:

Figura 25 Diagrama de estructura de código de MyBatis

  • Modo de combinación: como ChooseSqlNode, IfSqlNode, etc.
  • Patrón de método de plantilla: como BaseExecutor y SimpleExecutor, pero también BaseTypeHandler y todas las subclases como IntegerTypeHandler
  • Modo constructor: como SqlSessionFactoryBuilder, XMLConfigBuilder, XMLMapperBuilder, XMLStatementBuilder, CacheBuilder
  • Modo de fábrica: como SqlSessionFactory, ObjectFactory, MapperProxyFactory
  • Modo proxy: el núcleo de la implementación de MyBatis, como MapperProxy, ConnectionLogger

4 Referencia de la documentación

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

{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

Origin my.oschina.net/u/4090830/blog/5581667
Recomendado
Clasificación