O princípio de consulta subjacente do MyBatis para aprendizado de código-fonte

Leitura guiada

Este artigo começa com um bug de versão baixa do MyBatis (versão anterior a 3.4.5), analisa um processo de consulta completo do MyBatis e interpreta um processo de consulta do MyBatis em detalhes desde a análise do arquivo de configuração até o processo de execução completo de uma consulta . Saiba mais sobre o processo de consulta única do MyBatis. Na escrita de código usual, um bug em uma versão baixa do MyBatis (a versão anterior a 3.4.5) foi encontrado. Como a versão em muitos projetos é inferior a 3.4.5, ele é reproduzido aqui com um exemplo simples. analisar o processo de consulta do MyBatis a partir da perspectiva do código-fonte, para que todos possam entender o princípio de consulta do MyBatis.

 

1 Fenômeno do problema

1.1 Reprodução do problema do cenário

Conforme mostrado na figura abaixo, no Mapper de exemplo, um método queryStudents é fornecido abaixo para consultar os dados que atendem às condições de consulta da tabela do aluno. O parâmetro de entrada pode ser nome_aluno ou uma coleção de nome_aluno. No exemplo, o parâmetro só passa na lista de studentName.

 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>

O resultado esperado da corrida é

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

Mas o resultado real da corrida é

==> Preparando: selecione * from student WHERE student_name IN ( ? , ? ) AND student_name = ?

==> Parâmetros: lct(String), lct2(String), lct2(String)

<== Colunas: id, nome_do_aluno, idade

<== Linha: 2, lct2, 2

<== Total: 1

Pode-se ver pelos resultados em execução que o nome_aluno não recebe um valor separado, mas após a análise de MyBatis, um valor é atribuído apenas ao nome_aluno. Pode-se inferir que MyBatis tem um problema em analisar SQL e atribuir valores a O palpite inicial é o loop foreach. O valor da variável no foreach é trazido para fora do foreach, resultando em uma exceção na análise SQL.

2 Princípio de consulta MyBatis

2.1 Arquitetura MyBatis

2.1.1 Diagrama de Arquitetura

Vamos dar uma breve olhada no modelo geral de arquitetura do MyBatis. No geral, o MyBatis é dividido principalmente em quatro módulos:

Camada de interface : a principal função é lidar com o banco de dados

Camada de processamento de dados : A camada de processamento de dados pode ser considerada o núcleo do MyBatis e tem duas funções:

  • Construa instruções SQL dinâmicas passando parâmetros;
  • Execução de instruções SQL e encapsulamento de resultados de consulta para integrar List<E>

Camada de suporte do framework : inclui principalmente gerenciamento de transações, gerenciamento de pool de conexões, mecanismo de cache e configuração de instruções SQL

Camada de inicialização : A camada de inicialização é a maneira de configurar e iniciar as informações de configuração do MyBatis. O MyBatis fornece duas maneiras de guiar o MyBatis: baseado em arquivo de configuração XML e baseado em API Java

2.1.2 Quatro Objetos MyBatis

Existem quatro objetos principais que percorrem todo o framework MyBatis, ParameterHandler, ResultSetHandler, StatementHandler e Executor. Os quatro objetos percorrem o processo de execução de todo o framework. As principais funções dos quatro objetos são:

  • ParameterHandler: defina parâmetros pré-compilados
  • ResultSetHandler: manipula o conjunto de resultados retornado do SQL
  • StatementHandler: lida com pré-compilação de instruções sql, configuração de parâmetros e outros trabalhos relacionados
  • Executor: O executor do MyBatis, usado para realizar operações de adição, exclusão, modificação e consulta

2.2 Interpretar um processo de consulta do MyBatis a partir do código-fonte

Primeiro dê o código para reproduzir o problema e o processo de preparação correspondente

2.2.1 Preparação de Dados

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 Preparação do código

1.arquivo de configuração do 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 exemplo

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álise do Processo de Consulta

1. Construção de SqlSessionFactory

Primeiro, observe o processo de criação do objeto SqlSessionFactory

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

O código primeiro obtém o objeto chamando o método build em SqlSessionFactoryBuilder e insere o método build

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

chamar seu próprio método de compilação

Figura 1 O próprio método de compilação chama a legenda de depuração

Nesse método, um objeto XMLConfigBuilder será criado para analisar o arquivo de configuração MyBatis de entrada e, em seguida, o método de análise será chamado para análise.

Figura 2 legenda de depuração do parâmetro de entrada de análise de análise

Neste método, o conteúdo de xml será obtido do diretório raiz do arquivo de configuração do MyBatis. O objeto analisador é um objeto XPathParser, que é especialmente usado para analisar arquivos xml. Como obter cada nó do arquivo xml? Não mais explicações são fornecidas aqui. Aqui você pode ver que a análise do arquivo de configuração começa a partir do nó de configuração, que também é o nó raiz no arquivo de configuração 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>

Em seguida, passe o arquivo xml analisado para o método parseConfiguration, no qual será obtida a configuração de cada nó no arquivo de configuração

Figura 3 Legenda de depuração de configuração de análise

Para obter a configuração do nó dos mapeadores para ver o processo de análise específico

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

Insira o método mapperElement

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

Figura 4 legenda de depuração do método mapperElement

Veja que MyBatis ainda analisa o nó mappers criando um objeto XMLMapperBuilder, no método parse

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


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

Analise cada arquivo mapeador configurado chamando o 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);
  }
}

Vamos dar uma olhada em como analisar um arquivo mapeador analisando as tags CRUD no mapeador

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

Pode-se observar que o MyBatis ainda analisa os nós de adição, exclusão, modificação e consulta criando um objeto XMLStatementBuilder. Ao chamar o método parseStatementNode deste objeto, todas as informações de configuração configuradas nesta tag serão obtidas neste método, e então definidas.

Figura 5 legenda de depuração do método parseStatementNode

Após a conclusão da análise, adicione todas as configurações a um MappedStatement por meio do método addMappedStatement e adicione o mappedstatement à configuração

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

 

Você pode ver que uma declaração mapeada contém os detalhes de uma tag CRUD

Figura 7 Legenda de depuração do método do objeto Mappedstatement

E uma configuração contém todas as informações de configuração, incluindo mapperRegistertry e mappedStatements

Figura 8 Legenda de depuração do método do objeto de configuração

processo específico

Figura 9 O processo de construção do objeto SqlSessionFactoryFigura 9 O processo de construção do objeto SqlSessionFactory

2. Processo de criação SqlSession

Após a criação da SqlSessionFactory, vamos dar uma olhada no processo de criação da SqlSession

SqlSession sqlSession = sqlSessionFactory.openSession();

Primeiro, o método openSessionFromDataSource de DefaultSqlSessionFactory será chamado

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

Nesse método, as propriedades como DataSource são obtidas primeiro da configuração para formar o objeto Environment, e um objeto de transação TransactionFactory é construído usando as propriedades no 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();
  }
}

Após a criação da transação, o objeto Executor é criado. A criação do objeto Executor é baseada no executorType. O padrão é o tipo SIMPLE. Se não houver configuração, o SimpleExecutor é criado. Se o cache de segundo nível estiver habilitado, o CachingExecutor será criado.

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

Após a criação do executor, o método executor = (Executor)
interceptorChain.pluginAll(executor) será executado. O significado correspondente desse método é usar cada interceptor para encapsular e retornar o executor e, finalmente, chamar o método DefaultSqlSession para criar um SqlSession

Figura 10 O processo de criação do objeto SqlSession

3. O processo de aquisição da Mapper

Com SqlSessionFactory e SqlSession, você precisa obter o Mapper correspondente e executar os métodos no mapeador

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

Na primeira etapa, sabemos que todos os mapeadores são colocados no objeto MapperRegistry, portanto,
obtenha o mapeador correspondente chamando o 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);
  }
}

Em MyBatis, todos os mapeadores correspondem a uma classe proxy, após obter a classe proxy correspondente ao mapeador, execute o método newInstance para obter a instância correspondente, para que o método possa ser chamado através desta instância.

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


}

O processo de obtenção do mapeador é

Figura 11 O processo de aquisição do Mapper

4. Processo de consulta

Depois de obter o mapeador, você pode chamar o método específico

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

Primeiro
, o método de org.apache.ibatis.binding.MapperProxy#invoke será chamado. Nesse método, org.apache.ibatis.binding.MapperMethod#execute será chamado

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

Primeiro, determine qual método executar de acordo com o tipo de SQL, adicione, exclua, modifique e verifique. Aqui, o método SELECT é executado. Em SELECT, qual método executar é determinado de acordo com o tipo de valor de retorno do método. pode ser visto que não existe um método separado para selectone em select, mas selectList é usado
. .Object) 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();
  }
}

Em selectList, primeiro obtenha MappedStatement do objeto de configuração, que contém as informações relevantes do Mapper na instrução e, em seguida, chame o
método org.apache.ibatis.executor.CachingExecutor#query()

Figura 12 Diagrama de depuração do método query()

Neste método, o SQL é primeiro analisado e o SQL é emendado de acordo com os parâmetros de entrada e o SQL original.

Figura 13 Diagrama de código do processo de emenda SQL

O SQL finalmente analisado chamando getBoundSql em MapperedStatement é

Figura 14 Diagrama do resultado do processo de splicing SQL

Em seguida, chame
org.apache.ibatis.parsing.GenericTokenParser#parse para analisar o SQL analisado

Figura 15 Diagrama do processo de análise SQL

O resultado final da análise é

Figura 16 Diagrama de resultados da análise SQL

Por fim, será chamado o método doQuery no SimpleExecutor, neste método será obtido o StatementHandler e, em seguida
, será chamado o método org.apache.ibatis.executor.statement.PreparedStatementHandler#parameterize para processar parâmetros e SQL, e por fim o método execute o método da instrução será chamado para obter o conjunto de resultados e, em seguida, use o resultHandler para processar o nó

Figura 17 Diagrama de resultados do processamento SQL

O processo principal da consulta é

 

Figura 18 Diagrama de processamento do fluxo de consulta

5. Resumo do processo de consulta

Todo o processo de consulta é resumido da seguinte forma

Figura 19 Abstração do processo de consulta

2.3 Cenário de causas e soluções de problemas

2.3.1 Investigação pessoal

O local em que esse bug aparece é que, ao vincular parâmetros SQL, a localização no código-fonte é

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

Como o SQL escrito é um SQL com parâmetros vinculados dinamicamente, ele eventualmente irá para
o 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;
}

Neste método, o método rootSqlNode.apply(context) será chamado. Como essa tag é uma tag foreach, o método apply será chamado para
o 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;
}

Quando o método appItm é chamado, os parâmetros são vinculados e os problemas de variáveis ​​dos parâmetros existirão na área de parâmetros de vinculações

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

Ao vincular parâmetros, ao vincular o método foreach, você pode ver que não apenas os dois parâmetros no foreach estão vinculados nas vinculações, mas também um nome de parâmetro adicional StudentName->lct2, o que significa que o último parâmetro também será Aparece em o parâmetro de vinculações,

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

 

Figura 20 Processo de vinculação de parâmetros

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

Pode-se ver que quando o métodovaluBoolean é chamado, o context.getBindings() é o parâmetro bindings mencionado acima e passado, pois agora existe um studentName neste parâmetro, então ao usar a expressão Ognl, ele é determinado assim se a tag for valiosa, então esta tag será analisada

Figura 21 Processo de ligação de parâmetro único

O resultado final da ligação é

Figura 22 Todo o processo de vinculação de parâmetros

Portanto, há um problema com o parâmetro de ligação neste local e o problema foi encontrado até agora.

2.3.2 Explicação oficial

Leia a documentação oficial do MyBatis para verificação e descobri que existe tal frase nas correções de bugs no lançamento da versão 3.4.5

Figura 23 Este problema foi corrigido oficialmente no registro do github Figura 23 Este problema foi corrigido oficialmente no registro do github

Corrigido um bug na modificação do contexto da variável global na versão foreach

O endereço do problema é https://github.com/mybatis/mybatis-3/pull/966

A correção é https://github.com/mybatis/mybatis-3/pull/966/commits/84513f915a9dcb97fc1d602e0c06e11a1eef4d6a

 

Você pode ver o plano de modificação oficial, redefinindo um objeto para armazenar variáveis ​​globais e variáveis ​​locais, respectivamente, o que resolverá o problema de que foreach alterará variáveis ​​globais.

Figura 24 Um exemplo do código de reparo oficial para este problema

2.3.3 Plano de reparo

  • Atualize a versão do MyBatis para acima de 3.4.5
  • Se a versão permanecer inalterada, o nome da variável definida em foreach não deve ser consistente com o externo

3 Resumo do processo de leitura do código fonte

O diretório do código-fonte do MyBatis é relativamente claro, basicamente todos os módulos com a mesma função estão juntos, mas se você ler o código-fonte diretamente, ainda pode ser difícil entender seu processo de execução, desta vez através de um simples O processo de consulta é seguido do início ao fim, e você pode ver o fluxo de design e processamento do MyBatis, como os padrões de design usados ​​nele:

Figura 25 Diagrama de estrutura de código MyBatis

  • Modo de combinação: como ChooseSqlNode, IfSqlNode, etc.
  • Padrão de método de modelo: como BaseExecutor e SimpleExecutor, mas também BaseTypeHandler e todas as subclasses, como IntegerTypeHandler
  • Modo construtor: como SqlSessionFactoryBuilder, XMLConfigBuilder, XMLMapperBuilder, XMLStatementBuilder, CacheBuilder
  • Modo de fábrica: como SqlSessionFactory, ObjectFactory, MapperProxyFactory
  • Modo proxy: o núcleo da implementação do MyBatis, como MapperProxy, ConnectionLogger

4 Referência de Documentação

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

{{o.name}}
{{m.name}}

Acho que você gosta

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