[Rasgando o código-fonte do MyBatis manualmente] Análise de todo o processo de SQL dinâmico

Visão geral do SQL dinâmico

O SQL dinâmico é uma das funções poderosas do MyBatis. Ele elimina o problema de montar strings SQL no código JAVA e, ao mesmo tempo, mantém nosso controle independente sobre o SQL, o que é mais conveniente para a otimização e transformação do desempenho do SQL.

No SQL dinâmico, usamos elementos de script XML para controlar a montagem do SQL. Esses são elementos usados ​​no desenvolvimento diário. Vamos analisá-los juntos.

  • se
  • escolher (quando, caso contrário)
  • aparar (onde, definir)
  • para cada

insira a descrição da imagem aqui

se

<if test="title != null">
    AND title like #{title}
</if>

Aceite uma expressão lógica OGNL por meio de teste no elemento if, que pode ser usado para cálculos lógicos convencionais, como: julgamento vazio, tamanho e, ou e cálculos para subatributos.

escolha(quando、caso contrário)

escolha é usado para selecionar uma das várias condições e, se nenhuma for atendida, use o valor em caso contrário. Semelhante ao switch em java. Claro, esse tipo de lógica também pode ser realizado com if, mas a expressão lógica é relativamente complicada. Além disso, não há elemento else correspondente no elemento if.

aparar (onde, definir)

trim é usado para resolver o problema de instruções SQL extras após a montagem do SQL, como no exemplo a seguir:

<select id="findBlog"
     resultType="Blog">
  SELECT * FROM BLOG
  WHERE
  <if test="state != null">
   AND state = #{state}
  </if>
</select>

Se a condição if for satisfeita, um SQL será gerado finalmente, com um caractere AND adicional na sintaxe

SELECT * FROM BLOG  WHERE  AND state = #{state}

Se não for satisfeito, o SQL também estará errado e haverá um WHERE extra na sintaxe

  SELECT * FROM BLOG  WHERE 

O elemento where apenas inserirá uma cláusula "WHERE" se o elemento filho não retornar nada. Além disso, se as cláusulas começarem com "AND" ou "OR", o elemento where também as removerá. onde o elemento é equivalente ao seguinte elemento de acabamento

<trim prefix="WHERE" prefixOverrides="AND |OR ">
  ...
</trim>

O elemento Set é usado para o problema de vírgula extra ao modificar vários campos, o que é equivalente ao seguinte elemento trim

<trim prefix="SET" suffixOverrides=",">
  ...
</trim>

para cada

Esse elemento é usado para percorrer o valor da coleção, como construir várias condições de entrada ou adicionar e modificar o processamento em lote. Ele permite que você especifique uma coleção, declarando itens de coleção (item) e variáveis ​​de índice (index) que podem ser usadas no corpo do elemento.

expressão OGNL

O nome completo de OGNL é Object Graph Navigation Language (Object Graph Navigation Language), que é uma linguagem de expressão JAVA, que é usada por MyBatis para realizar a função de SQL dinâmico.

A principal função do OGNL é obter ou julgar o valor de acordo com as propriedades do objeto. Ele pode acessar facilmente o valor dentro do objeto de acordo com o caminho da propriedade do objeto.

As principais funções que o OGNL pode realizar são:

  1. Acesse as propriedades do objeto: user.name
  2. Método de chamada: user.getName()
  3. Acesse os elementos da coleção: user.orders[0]
  4. Determine se existe um atributo ou método: user.name?
  5. Chame um método ou propriedade de classe estática: @java.lang.Math@PI
  6. Implemente operações de comparação e aritméticas: user.age > 18
  7. Realize a operação lógica: user.name && user.age < 30

Portanto, OGNL nos fornece uma maneira fácil de manipular e julgar o valor das propriedades do objeto. Podemos "."implementar facilmente várias operações nas propriedades do objeto por meio de.

MyBatis usa OGNL para implementar várias funções de SQL dinâmico:

  1. if: Determine se o resultado da expressão OGNL fornecida é verdadeiro e, se for verdadeiro, adicione o conteúdo do elemento atual.
    <if test="name != null">
      name = #{name}  
    </if>
    
  2. escolha, quando, caso contrário: equivalente à instrução switch em Java, qual conteúdo de elemento incluir é determinado de acordo com a expressão OGNL no primeiro quando for satisfeita.
    <choose>
      <when test="age > 18">
        age > 18
      </when>
      <when test="age == 18">
        age == 18 
      </when>
      <otherwise> 
        age < 18
      </otherwise>
    </choose> 
    
  3. trim: usado para processar o prefixo ou sufixo da string, onde prefixOverrides e suffixOverrides suportam expressões OGNL.
    <trim prefix="(" prefixOverrides="(" suffix=")" suffixOverrides=")">...</trim>
    
  4. where: A instrução condicional where usada para emendar sql dinamicamente removerá automaticamente o primeiro e ou ou. Entre eles, o teste suporta expressões OGNL.
    <where>
      <if test="name != null"> name = #{name} </if>  
      <if test="age != null"> and age = #{age} </if>
    </where>
    
  5. set: A declaração condicional set usada para unir sql dinamicamente removerá automaticamente a última vírgula. Entre eles, o teste suporta expressões OGNL.
    <set>
      <if test="name != null"> name = #{name}, </if>
      <if test="age != null"> age = #{age} </if> 
    </set> 
    
  6. forEach: usado para iterar a coleção, iterará sobre cada elemento da coleção e executará o conteúdo do corpo da tag
    <forEach items="${lists}" index="index" item="item" open="(" close=")" separator=",">
    #{item}  
    </forEach>
    

Pode-se ver que o OGNL fornece forte suporte para a implementação dinâmica de SQL do MyBatis. Por meio de uma simples expressão OGNL, podemos julgar se uma determinada condição foi atendida e decidir se incluímos um determinado fragmento sql, o que traz grande flexibilidade à função SQL dinâmica do MyBatis.

Portanto, o OGNL é a pedra angular da implementação do SQL dinâmico do MyBatis, permitindo que o MyBatis filtre e junte as instruções SQL necessárias de acordo com as condições, não apenas emendas simples, o que torna as instruções SQL geradas pelo MyBatis extremamente flexíveis e poderosas.

Ao mesmo tempo, também introduzimos uma classe de ferramentas:ExpressionEvaluator

Portanto, ExpressionEvaluator fornece principalmente duas funções:

  1. Analisar a expressão OGNL: analisa a string de expressão em um objeto Expression, que representa a estrutura semântica interna da expressão.
  2. Avaliar uma expressão OGNL: Use o objeto Expression e o contexto do gráfico do objeto especificado para avaliar o resultado final da expressão.

Ele fornece principalmente as seguintes APIs:

  • parseExpression(String expression): Analisa a expressão OGNL e retorna Expression.
  • avaliar (String expression, Object root): analisa e avalia diretamente a expressão, o objeto raiz é root e retorna o resultado.
  • avaliar(Expression expr, Object root): Use o objeto Expression expr e o objeto root root para avaliar a expressão e retornar o resultado.

Por meio dessas APIs, MyBatis pode analisar a expressão OGNL em Mapper.xml em Expression e, em seguida, usar o objeto de parâmetro para avaliar a Expression para obter o resultado final e decidir a próxima ação de acordo. Isso permite que o MyBatis suporte expressões OGNL de forma muito clara e flexível e realize as funções de SQL dinâmico e carregamento lento.

BoundSql

Quando olhamos para o código-fonte, muitas vezes vimos BoundSql, então o que é?
insira a descrição da imagem aqui

BoundSql não é uma instrução SQL diretamente executável . É uma classe POJO que encapsula informações sobre instruções SQL.

Ao chamarmos o método no Mapper, o MyBatis irá analisar o arquivo Mapper.xml correspondente, e gerar a instrução SQL final a ser executada de acordo com os parâmetros que passamos e as condições do SQL dinâmico. Esta instrução SQL será encapsulada no objeto BoundSql.

As informações contidas no BoundSql são:

  1. sql: a própria instrução SQL gerada.
  2. parameterMappings: informações de mapeamento de parâmetros na instrução sql. Por exemplo, #{name} será mapeado para o nome, etc.
  3. parâmetros: Mapa de valores de parâmetro de entrada, a chave é o nome do parâmetro e o valor é o valor do parâmetro.
    Então MyBatis usará o objeto MappedStatement em Configuração para executar este SQL, e MappedStatement também contém BoundSql.

Então, qual é a relação entre MappedStatement e BoundSql?

MappedStatement e BoundSql têm um relacionamento próximo. simplesmente coloque:

  • MappedStatement representa uma consulta ou informações de definição atualizadas em Mapper.xml. Ele contém o ID da instrução SQL, mapeamento de parâmetros e outras informações.
  • BoundSql representa os detalhes de uma instrução SQL a ser executada. Ele contém a instrução SQL real, mapeamento de parâmetros e valores de parâmetros.

Ao chamarmos o método da interface do Mapper, o MyBatis encontrará o MappedStatement correspondente de acordo com a assinatura do método. Em seguida, ele irá analisar a instrução SQL no MappedStatement e gerar o SQL final para ser executado de acordo com os parâmetros que passamos, e esse SQL será agrupado no objeto BoundSql.

então,Um MappedStatement corresponderá a vários BoundSql, dependendo dos diferentes parâmetros que passamos em cada chamada. Mas a mesma chamada irá gerar apenas um BoundSql

MappedStatement pode ser entendido como a definição estática de uma instrução por MyBatis, enquanto BoundSql é a informação específica da instrução gerada dinamicamente cada vez que é executada .

Sua relação pode ser expressa como:

  • Um MappedStatement corresponde a uma definição de consulta ou atualização em Mapper.xml.
  • Cada vez que o método da interface Mapper for chamado, um BoundSql será gerado.
  • Este BoundSql contém a instrução SQL final a ser executada e as informações de parâmetro geradas de acordo com o MappedStatement e os parâmetros de entrada.
  • MyBatis usará MappedStatement e este BoundSql para executar o SQL e retornar o resultado.

Análise do processo principal de SQL dinâmico

insira a descrição da imagem aqui

SqlNodeGenericName

SqlNode é uma interface usada para representar o fragmento SQL no MyBatis.

Normalmente, o texto de cada linha de SQL em cada tag de inserção/atualização/exclusão/seleção no Mapper.xml que escrevemos com base na estrutura MyBaits (incluindo tags de inclusão substituídas por SQL) é abstraído em SqlNode.

insira a descrição da imagem aqui

Cada elemento dinâmico terá uma classe de script correspondente. Por exemplo, if corresponde a ifSqlNode, forEarch corresponde a ForEachSqlNode e assim por diante. :

  • StaticTextSqlNode: instrução SQL pura, não contém nenhuma instrução SQL dinâmica, por exemplo:select * from user
  • TextSqlNode: instruções SQL contêm espaços reservados ${}, por exemploselect * from ${user}
  • IfSqlNode: a instrução SQL na submarca se/quando;
  • ChooseSqlNode: a instrução SQL na subtag de escolha;
  • ForEachSqlNode: a instrução SQL na subtag foreach;
  • VarDecSqlNode: a instrução SQL na subtag de ligação;
  • TrimSqlNode: a instrução SQL na subtag trim;
  • WhereSqlNode: a instrução SQL na subtag where;
  • SetSqlNode: a instrução SQL na subtag set;
  • MixedSqlNode: Se o texto SQL da tag inserir/atualizar/excluir/selecionar tiver mais de uma linha, reúna todos os SqlNodes.

insira a descrição da imagem aqui

A interface SqlNode define apenas um método booleano apply(DynamicContext context), que monta cada SqlNode em uma instrução SQL completa por meio do objeto DynamicContext.

Cada parte do SQL é um SqlNode, eles serão incluídos ou excluídos de acordo com o julgamento das condições e, finalmente, combinados em uma instrução SQL completa

Esses tipos de SqlNodes podem ser aninhados entre si para formar uma árvore SqlNode. O MyBatis gerará a instrução SQL final com base nesta árvore e nos valores dos parâmetros

DynamicContext

DynamicContext é como uma corda de varas de bambu e SqlNode é um pedaço de carne em uma vara de bambu. Toda a carne em uma vara de bambu é MixedSqlNode. A carne é amarrada através de varas de bambu para formar um delicioso churrasco - SQL! !

Por que não há condimentos no churrasco, assim como por que as instruções SQL carecem de parâmetros? Os parâmetros são armazenados no campo de vinculações em DynamicContext. Obtenha a instrução SQL concatenada do StringJoiner por meio do método getSql().

insira a descrição da imagem aqui

  • PARAMETER_OBJECT_KEY: Representa a Key do objeto parâmetro, que o MyBatis utilizará para obter do contexto o objeto parâmetro passado em tempo de compilação.
  • DATABASE_ID_KEY: indica a Chave de identificação do banco de dados, e o MyBatis utilizará esta Chave para obter a identificação do banco de dados atual a partir do contexto, como "MySQL".
  • ContextMap: representa o Mapa de contexto, e o MyBatis irá salvar diversos dados relacionados ao contexto atual neste Mapa, como objetos de parâmetros, identificadores de banco de dados, etc.
  • StringJoiner: Usado para unir e formatar fragmentos SQL em cláusulas WHERE ou SET, o que torna o SQL gerado mais limpo.
  • uniqueNumber: Representa um número inteiro, que é usado para gerar o alias do conjunto de resultados ao percorrer a coleção para evitar erros de sintaxe SQL.

Análise do código-fonte

StaticTextSqlNode

Como não contém nenhum SQL dinâmico, ele não depende de parâmetros reais para emendar instruções SQL

public class StaticTextSqlNodeDemo {
    
    
    public static void main(String[] args) {
    
    
        Configuration configuration = new Configuration();
        SqlNode staticTextSqlNode = new StaticTextSqlNode("SELECT * FROM user ");
        DynamicContext dynamicContext = new DynamicContext(configuration, null);
        staticTextSqlNode.apply(dynamicContext);
        String sql = dynamicContext.getSql();
        System.out.println(sql);
    }
}

insira a descrição da imagem aqui

Código-fonte de StaticTextSqlNode:

public class StaticTextSqlNode implements SqlNode {
    
    
  private final String text;

  public StaticTextSqlNode(String text) {
    
    
    this.text = text;
  }

  @Override
  public boolean apply(DynamicContext context) {
    
    
    context.appendSql(text);
    return true;
  }

}

O código fonte do StaticTextSqlNode é bem simples, ou seja, a instrução SQL é emendada atrás da instrução SQL anterior através do método appendSql() do DynamicContext.

TextSqlNode

Como a instrução SQL contém espaços reservados ${}, são necessários parâmetros para analisar os espaços reservados.

public class TextSqlNodeDemo {
    
    
    public static void main(String[] args) {
    
    
        Configuration configuration = new Configuration();
        Map<String, Object> paraMap = new HashMap<>();
        // 把注释放放开并把下面put 方法注解之后会发现解析 ${} 占位符的值为空字符串 
        // Map<String, Object> paraMap = null;
        paraMap.put("user", "haha");
		// paraMap.put("user", "'user'");
        SqlNode textSqlNode = new TextSqlNode("SELECT * FROM ${user}");
        DynamicContext dynamicContext = new DynamicContext(configuration, paraMap);
        textSqlNode.apply(dynamicContext);
        String sql = dynamicContext.getSql();
        System.out.println(sql);
    }
}

insira a descrição da imagem aqui

Código-fonte do TextSqlNode:

	@Override
	public boolean apply(DynamicContext context) {
    
    
		// 通过 createParse 获取 GenericTokenParser(通用令牌解析器) 对象(主要是解决 ${} 占位符)。
		// 如果发现 ${} 占位符则通过 BindingTokenParser 的 handleToken(String) 方法返回值替换 ${} 占位符
	  GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
	  context.appendSql(parser.parse(text));
	  return true;
	}

	@Override
	public String handleToken(String content) {
    
    
	  // 通过 DynamicContext 获取实参
	  Object parameter = context.getBindings().get("_parameter");
	  if (parameter == null) {
    
    
	    context.getBindings().put("value", null);
	  } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
    
    
	  	// SimpleTypeRegistry 中 SIMPLE_TYPE_SET 包含的类则存在 DynamicContext 参数中
	    context.getBindings().put("value", parameter);
	  }
	  // 通过 OGNL 从实参中获取 ${} 占位符的值
	  Object value = OgnlCache.getValue(content, context.getBindings());
	  String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
	  checkInjection(srtValue);
	  return srtValue;
	}

insira a descrição da imagem aqui

Você pode ver que nossos parâmetros são obtidos de um mapa em DynamicContext

Este GenericTokenParser é uma classe interna estática de TextSqlNode
insira a descrição da imagem aqui

IfSqlNode

A instrução SQL na subtag if/when é abstrata, e a instrução SQL na tag if é emendada apenas quando a expressão de teste na tag if é verdadeira.

public class IfSqlNodeDemo {
    
    
	public static void main(String[] args) {
    
    
		Configuration configuration = new Configuration();
		// 实参对象
		Map<String, Object> paraMap = new HashMap<>();
		paraMap.put("user", "user");
		SqlNode staticTextSqlNode = new StaticTextSqlNode("SELECT * FROM user");
		// 构建 IfSqlNode 对象,传入 if 标签里面的 SQL 抽象和 test 表达式
		SqlNode ifSqlNode = new IfSqlNode(staticTextSqlNode, "user != null");
		DynamicContext dynamicContext = new DynamicContext(configuration, paraMap);
		// 通过 DynamicContext 拼接 SQL
		ifSqlNode.apply(dynamicContext);
		// 获取 SQL 语句
		String sql = dynamicContext.getSql();
		// 控制台输出
		System.out.println(sql);
	}
}

insira a descrição da imagem aqui

Código-fonte do IfSqlNode:
insira a descrição da imagem aqui

	@Override
	public boolean apply(DynamicContext context) {
    
    
		// 通过 OGNL 判断 test 表达式是否成立,表达式里面涉及的属性值通过
		//  DynamicContext 传入的实参获取。如果成立折拼接 SQL 语句
		if (evaluator.evaluateBoolean(test, context.getBindings())) {
    
    
		  contents.apply(context);
		  return true;
		}
		return false;
	}

ChooseSqlNode

A instrução SQL na subtag select é abstrata. Quando a expressão de teste na tag when é estabelecida, a instrução SQL contida nela será emendada, caso contrário, a instrução SQL na tag else será usada. Semelhante à instrução if...else if...else em Java, apenas uma lógica de ramificação é executada.

public class ChooseSqlNodeDemo {
    
    
	public static void main(String[] args) {
    
    
		Configuration configuration = new Configuration();
		// 实参对象
		Map<String, Object> paraMap = new HashMap<>();
		paraMap.put("name", "文海");
		SqlNode staticTextSqlNode = new StaticTextSqlNode("SELECT * FROM user WHERE 1 = 1");
		// 构建 IfSqlNode 对象,传入 if 标签里面的 SQL 抽象和 test 表达式
		SqlNode ifSqlNode = new IfSqlNode(new StaticTextSqlNode(" AND name = #{name}"), "name != null");
		SqlNode defaultSqlNode = new StaticTextSqlNode(" AND name = 'wenhai'");
		DynamicContext dynamicContext = new DynamicContext(configuration, paraMap);
		// 通过 DynamicContext 拼接 SQL
		staticTextSqlNode.apply(dynamicContext);
		// 通过 DynamicContext 拼接 SQL
		ChooseSqlNode chooseSqlNode = new ChooseSqlNode(Collections.singletonList(ifSqlNode), defaultSqlNode);
		chooseSqlNode.apply(dynamicContext);
		// 获取 SQL 语句
		String sql = dynamicContext.getSql();
		// 控制台输出
		System.out.println(sql);
	}
}

Código-fonte de ChooseSqlNode:

	// 通过构造函数传入 when 标签 SQL 抽象和 otherwise 标签的 SQL 抽象
	public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
    
    
	  this.ifSqlNodes = ifSqlNodes;
	  this.defaultSqlNode = defaultSqlNode;
	}
	
	@Override
	public boolean apply(DynamicContext context) {
    
    
		// 如果一个分支条件满足就不再执行后面的逻辑
		for (SqlNode sqlNode : ifSqlNodes) {
    
    
		  if (sqlNode.apply(context)) {
    
    
		    return true;
		  }
		}
		// 前面的 when 标签里面的表达式都不满足,并且有兜底的 otherwise 标签则拼接里面的 SQL
		if (defaultSqlNode != null) {
    
    
		  defaultSqlNode.apply(context);
		  return true;
		}
		return false;
	}

ForCadaSqlNode

A abstração SQL na subtag foreach pode obter o valor correspondente através das variáveis ​​definidas no item e índice na tag. index é o valor do índice da matriz e da coleção e o tipo Map é o valor na chave, item é o elemento na matriz e na coleção e o tipo Map é o valor no valor.

public class ForeachSqlNodeDemo {
    
    
    public static void main(String[] args) {
    
    
        Configuration configuration = new Configuration();
        // 实参对象
        Map<String, Object> paraMap = new HashMap<>();
//        Map<String, String> param = new HashMap<>();
//        param.put("wenhai", "文海");
//        param.put("wenhai2", "文海2");
//        paraMap.put("map", param);
        List<String> list = new ArrayList<>();
        list.add("wenhai");
        list.add("wenhai2");
        paraMap.put("list", list);
        DynamicContext dynamicContext = new DynamicContext(configuration, paraMap);
        SqlNode staticTextSqlNode = new StaticTextSqlNode("SELECT * FROM user WHERE name in");
        // 通过 DynamicContext 拼接 SQL
        staticTextSqlNode.apply(dynamicContext);
//        String collection = "map";
        String collection = "list";
        String item = "item";
        String index = "index";
        String open = "(";
        String close = ")";
        String separator = ",";
        ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, new StaticTextSqlNode("#{index}"), collection, index, item, open, close, separator);

        forEachSqlNode.apply(dynamicContext);
        // 获取 SQL 语句
        String sql = dynamicContext.getSql();
        // 控制台输出 :SELECT * FROM user WHERE name in (  #{__frch_index_0} , #{__frch_index_1} )
        // 同时 DynamicContext 里面的 _parameter 多出以  __frch_#index_n 和 __frch_#item_n 属性值
        // 便于后续通过
        System.out.println(sql);
    }
}

Código-fonte de ForEachSqlNode:

	/**
	 * ForEachSqlNode 构造函数
	 * 
	 * @param configuration			  全局 Configuration 对象
	 * @param contents                foreach 标签里面的 SQL 抽象
	 * @param collectionExpression    foreach 标签里面的 collection 属性值
	 * @param index					  foreach 标签里面的 index 属性值
	 * @param item					  foreach 标签里面的 item 属性值
	 * @param open					  foreach 标签里面的 open 属性值
	 * @param close				      foreach 标签里面的 close 属性值
	 * @param separator               foreach 标签里面的 separator 属性值
	 */
	public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, String item, String open, String close, String separator) {
    
    
	   this.evaluator = new ExpressionEvaluator();
	   this.collectionExpression = collectionExpression;
	   this.contents = contents;
	   this.open = open;
	   this.close = close;
	   this.separator = separator;
	   this.index = index;
	   this.item = item;
	   this.configuration = configuration;
	 }


	@Override
	public boolean apply(DynamicContext context) {
    
    
	  // 获取参数列表
	  Map<String, Object> bindings = context.getBindings();
	  // 通过 OGNL 获取 collectionExpression 表达式的值,该值不能为 null,
	  // 只能是 Iterable 实例和数组已经 Map 实例,其他都会报错
	  final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
	  if (!iterable.iterator().hasNext()) {
    
    
	    return true;
	  }
	  // 是否是第一次,第一次不用拼接 separator 值
	  boolean first = true;
	  // 如果设置了 open 属性值,则先拼接 open 属性值
	  applyOpen(context);
	  int i = 0;
	  for (Object o : iterable) {
    
    
	    DynamicContext oldContext = context;
	    // 如果是第一次或者是分隔符没有设置则通过 PrefixedContext 包装 DynamicContext 对象
	    // 在 appendSql 方法进行拼接 SQL 时候加上设置的前缀(此处就是 “”)
	    if (first || separator == null) {
    
    
	      context = new PrefixedContext(context, "");
	    } else {
    
    
	      context = new PrefixedContext(context, separator);
	    }
	    // 获取唯一序列号递增用于集合的索引
	    int uniqueNumber = context.getUniqueNumber();
	    // 为 DynamicContext 中的类型为 ContextMap 属性保存 foreach 遍历对应的值
	    // 以 __frch_#{index}_uniqueNumber 和 __frch_#{item}_uniqueNumber 为 key
	    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);
	    }
	    // 通过 FilteredDynamicContext 包装 PrefixedContext 替换 foreach 标签里面
	    // 以 #{} 占位符并且使用正则表达式匹配 item 以及 index 属性值为 __frch_#{index}_uniqueNumber 和 __frch_#{item}_uniqueNumber
	    contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
	    if (first) {
    
    
	      first = !((PrefixedContext) context).isPrefixApplied();
	    }
	    context = oldContext;
	    i++;
	  }
	  // 如果 foreach 标签里面的 close 属性设置了则拼接在 SQL 语句后面
	  applyClose(context);
	  context.getBindings().remove(item);
	  context.getBindings().remove(index);
	  return true;
	}

O SqlNode restante não será analisado, eles são todos semelhantes e o efeito é obtido envolvendo o DynamicContext

Estrutura dinâmica do script

Há um relacionamento aninhado entre os scripts. Por exemplo, o elemento if conterá um MixedSqlNode e o MixedSqlNode conterá 1 para 1 ou mais outros nós. Finalmente, uma árvore de sintaxe de script é formada. Conforme mostrado abaixo, os elementos SQL à esquerda formam a árvore sintática à direita.Na parte inferior do nó deve haver um StaticTextNode ou TextNode

insira a descrição da imagem aqui

Execução de script dinâmico

A interface do SqlNode é bem simples, existe apenas um método apply, a função do método é executar a lógica do nó do script atual, e aplicar o resultado no DynamicContext.

public interface SqlNode {
  boolean apply(DynamicContext context);
}

Por exemplo, ao executar apply em IfSqlNode, a lógica If é calculada primeiro e, se passar, continuará visitando seus nós filhos. Adicione o texto SQL ao DynamicContext até que o TextNode seja finalmente acessado. Por meio desse método recursivo semelhante, o Contexto visitará todos os nós e anexará o texto SQL qualificado final ao Contexto.

//IfSqlNode
public boolean apply(DynamicContext context) {
    
    //计算if表达示
  if (evaluator.evaluateBoolean(test, context.getBindings())) {
    
    
    contents.apply(context);
    return true;
  }
  return false;
}
//StaticTextSqlNode
public boolean apply(DynamicContext context) {
    
    
  context.appendSql(text);
  return true;
}

insira a descrição da imagem aqui
Depois de visitar todos os nós, uma string SQL será gerada, mas este não é um SQL executável diretamente, porque os parâmetros internos ainda estão na forma de expressões #{name=name}, você precisa usar o SqlSourceBuilder para construir o SQL executável e o mapeamento de parâmetros ParameterMapping. Só então o BoundSql pode ser gerado. A figura abaixo mostra que BoundSql é gerado após todos os nós serem executados no contexto.

insira a descrição da imagem aqui
Olhando para o código-fonte, durante o processo de SQL dinâmico para BoundSql, houve uma geração de StaticSqlSource no meio? Por que você deseja fazer isso e onde o conjunto SqlNode é analisado a partir do XML armazenado? Há também um novo conceito de fonte SQL SqlSource aqui.

SqlSourceName

SqlNode conecta SQL entre SqlNodes através de DynamicContext.Na verdade, esta função é realizada através de SqlSource. Obtenha BoundSql por meio do método getBoundSql() da interface SqlSource, e BoundSql contém instruções SQL completas, listas de parâmetros e parâmetros reais. SqlSource representa o conteúdo de uma instrução mapeada lida de um arquivo ou anotação XML. O SQL criado será passado para o banco de dados com base nos parâmetros de entrada recebidos do usuário.

Qual é a relação entre SqlSource e SqlNode?

  • Em MyBatis, SqlSource e SqlNode são classes usadas para encapsular instruções SQL.
  • SqlNode é MyStaticTextSqlNode, IfSqlNode, WhereSqlNode, etc., e cada SqlNode processará um fragmento SQL. Esses SqlNodes podem ser combinados para formar uma estrutura em árvore de instruções SQL.
  • O SqlSource costura a estrutura em árvore das instruções SQL em uma instrução SQL completa e fornece a função de mapeamento de parâmetros. SqlSource geralmente é composto por SqlNode. Ao analisar a estrutura em árvore das instruções SQL no arquivo Mapper.xml, o SQL emendado final é passado para o executor JDBC.
  • portanto,SqlNode é um produto intermediário que gera SqlSource. O SqlNode forma uma instrução SQL estruturada em árvore analisando o arquivo Mapper.xml e o SqlSource une essas instruções SQL em um SQL completo, executa o mapeamento de parâmetros e finalmente o entrega ao executor JDBC para execução.

SqlSource tem as seguintes categorias:

  • DynamicSqlSource: SQL para 动态 SQLe ${}espaços reservados
  • RawSqlSource: #{}SQL para espaços reservados
  • ProviderSqlSource: @*ProviderSQL fornecido para anotações
  • StaticSqlSource: contém apenas ?SQL com espaços reservados

Quando Mybatis processa #{}, ele substitui #{} em sql por um número ? e chama o método set de PreparedStatement para atribuir um valor; quando
Mybatis processa ${}, ele o substitui ${}pelo valor da variável.

insira a descrição da imagem aqui

StaticSqlSource

/**
 * {@link StaticSqlSource} 实例里面的 SQL 语句仅包含 ? 占位符。
 *
 * @author wenhai
 * @date 2021/7/20
 * @see SqlSource
 * @see RawSqlSource
 * @see StaticSqlSource
 * @see DynamicSqlSource
 * @see ProviderSqlSource
 */
public class StaticSqlSourceDemo {
    
    
    public static void main(String[] args) {
    
    
        Configuration configuration = new Configuration();
        String sql = "SELECT * FROM user WHERE id = #{id}";
        SqlSource staticSqlSource = new StaticSqlSource(configuration, sql);
        BoundSql boundSql = staticSqlSource.getBoundSql(5L);
        System.out.println(boundSql.getSql());
    }
}

Execute a saída do console do programa acima SELECT * FROM user WHERE id = #{id}sem nenhum processamento.

public class StaticSqlSource implements SqlSource {
    
    
  // SQL 语句
  private final String sql;
  // 参数映射列表
  private final List<ParameterMapping> parameterMappings;
  // 全局 Configuration 对象
  private final Configuration configuration;

  public StaticSqlSource(Configuration configuration, String sql) {
    
    
    this(configuration, sql, null);
  }

  public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
    
    
    this.sql = sql;
    this.parameterMappings = parameterMappings;
    this.configuration = configuration;
  }

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    
    
  	// 直接构建 BoundSql 对象返回
    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
  }

}

Pode ser visto no método StaticSqlSource#getBoundSql que a instrução SQL original não será processada quando o objeto BoundSql for obtido.

DynamicSqlSource

/**
 * {@link DynamicSqlSource} 包含动态 SQL 和 ${} 占位符
 *
 * @author wenhai
 * @date 2021/7/20
 * @see SqlSource
 * @see RawSqlSource
 * @see StaticSqlSource
 * @see DynamicSqlSource
 * @see ProviderSqlSource
 */
public class DynamicSqlSourceDemo {
    
    
    public static void main(String[] args) {
    
    
        Configuration configuration = new Configuration();
        // 实参对象
        Map<String, Object> paraMap = new HashMap<>();
        List<String> list = new ArrayList<>();
        list.add("wenhai");
        list.add("wenhai2");
        paraMap.put("list", list);
        paraMap.put("id", 5);
        SqlNode staticTextSqlNode = new StaticTextSqlNode("SELECT * FROM user WHERE");
        SqlNode textSqlNode = new TextSqlNode(" id = ${id} AND name IN");
        String collection = "list";
        String item = "item";
        String index = "index";
        String open = "(";
        String close = ")";
        String separator = ",";
        ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, new StaticTextSqlNode("#{item}"), collection, index, item, open, close, separator);
        SqlNode mixedSqlNode = new MixedSqlNode(Arrays.asList(staticTextSqlNode, textSqlNode, forEachSqlNode));
        SqlSource sqlSource = new DynamicSqlSource(configuration, mixedSqlNode);
        BoundSql boundSql = sqlSource.getBoundSql(paraMap);
        System.out.println(boundSql.getSql());
    }
}

Execute a saída do console do programa acimaSELECT * FROM user WHERE id = 5 AND name IN ( ? , ? )

public class DynamicSqlSource implements SqlSource {
    
    

  private final Configuration configuration;
  private final SqlNode rootSqlNode;

  public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
    
    
    this.configuration = configuration;
    this.rootSqlNode = rootSqlNode;
  }

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    
    
  	// 构建 DynamicContext 对象来处理 SqlNode
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    // 通过 SqlSourceBuilder#parse 方法来处理通过 DynamicContext 拼接过的 SQL
    // 主要处理 #{} 占位符替换成 ? 占位符和获取 ParameterMapping 列表
    // 构建 StaticSqlSource 对象
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    // 设置参数比如 foreach 标签的里面的额外参数等
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }

}

O SqlNode é processado quando o objeto BoundSql é obtido por meio do método DynamicSqlSouce#getBoundSql(). Se for SQL dinâmico e a instrução SQL que significa ${} espaço reservado, una e substitua de acordo com os parâmetros reais de entrada. Se for #{} espaço reservado? Substitua e, finalmente, crie BoundSql por meio de StaticSqlSource.

RawSqlSource

/**
 * {@link RawSqlSource} 不包含动态 SQL 和 ${} 占位符
 *
 * @author wenhai
 * @date 2021/7/20
 * @see SqlSource
 * @see RawSqlSource
 * @see StaticSqlSource
 * @see DynamicSqlSource
 * @see ProviderSqlSource
 */
public class RawSqlSourceDemo {
    
    
    public static void main(String[] args) {
    
    
        Configuration configuration = new Configuration();
        SqlNode sqlNode = new StaticTextSqlNode("SELECT * FROM user WHERE id = #{id}");
        SqlSource sqlSource = new RawSqlSource(configuration, sqlNode, Long.class);
        System.out.println(sqlSource.getBoundSql(5L).getSql());
    }
}

Execute a saída do console do programa acima SELECT * FROM user WHERE id = ?. O que acontecerá se o marcador de posição #{} for armazenado em cache para o marcador de posição ${} ou o SqlNode for substituído por outro SqlNode dinâmico?

public class RawSqlSource implements SqlSource {
    
    
  // 存储构建好的 StaticSqlSource 
  private final SqlSource sqlSource;

  public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
    
    
  	// 通过 getSql 方法获取 SQL 语句,此时没有传入实参,所以那些动态 SQL 和 ${} 占位符
  	// 无法处理,只能处理 #{} 占位符的 SqlNode
    this(configuration, getSql(configuration, rootSqlNode), parameterType);
  }

  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    
    
  	// 通过 SqlSourceBuilder#parse 方法替换 #{} 占位符为 ? 并构建 #{} 占位符的参数映射列表 
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
  }

  private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
    
    
    DynamicContext context = new DynamicContext(configuration, null);
    rootSqlNode.apply(context);
    return context.getSql();
  }

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    
    
  	// 直接通过 StaticSqlSource#getBoundSql 获取 BoundSql 实例
    return sqlSource.getBoundSql(parameterObject);
  }

}

As questões levantadas nos exemplos podem ser respondidas por meio da análise do código-fonte. Se for um espaço reservado ${}, não será processado e o SQL dinâmico poderá relatar um erro ou a instrução SQL processada poderá estar incompleta.

ProviderSqlSource

/**
 * {@link ProviderSqlSource} @*Provider 注解提供的 SQL
 *
 * @author wenhai
 * @date 2021/7/21
 * @see SqlSource
 * @see RawSqlSource
 * @see StaticSqlSource
 * @see DynamicSqlSource
 * @see ProviderSqlSource
 */

public class ProviderSqlSourceDemo {
    
    

    public static void main(String[] args) throws NoSuchMethodException {
    
    
        Configuration configuration = new Configuration();
        SelectProvider provider = UserMapper.class.getMethod("select", String.class).getAnnotation(SelectProvider.class);
        SqlSource providerSqlSource = new ProviderSqlSource(configuration, provider, null, null);
        System.out.println(providerSqlSource.getBoundSql("wenhai").getSql());
    }

    public String getSql() {
    
    
        return "SELECT * FROM user WHERE name = #{name}";
    }

    interface UserMapper {
    
    
        @SelectProvider(type = ProviderSqlSourceDemo.class, method = "getSql")
        List<User> select(String name);
    }
}

Execute a saída do console do programa acimaSELECT * FROM user WHERE name = ?

	@Override
	public BoundSql getBoundSql(Object parameterObject) {
    
    
	  // 通过 @*Provider 注解元信息通过反射调用方法拿到 SQL,
	  // 然后通过 XMLLanguageDriver#createSqlSource 方法解析 SQL 语句
	  // 获取 DynamicSqlSource/RawSqlSource -> StaticSqlSource
	  SqlSource sqlSource = createSqlSource(parameterObject);
	  return sqlSource.getBoundSql(parameterObject);
	}

Resumindo:
analise o SQL de acordo com a fonte SQL para obter o SqlNode e obtenha o SqlSource correspondente de acordo com o SqlNode, seja DynamicSqlSource ou RawSqlSource. Se for DynamicSqlSource unindo SQL dinâmico e processando espaços reservados ${} de acordo com os parâmetros reais e, em seguida, convertendo em StaticSqlSource por meio do método SqlSourceBuilder#parse(), e RawSqlSource foi convertido em StaticSqlSource por meio do método SqlSourceBuilder#parse() quando instanciado, que não depende de parâmetros reais, portanto, o desempenho é mais rápido que DynamicSqlSource . ProviderSqlSource obtém DynamicSqlSource ou RawSqlSource por meio do método XMLLanguageDriver#createSqlSource() após analisar a instrução SQL.

Resumir

Na definição superior, cada mapeamento Sql (MappedStatement) conterá um SqlSource para obter Sql executável (BoundSql). O SqlSource é dividido em fonte SQL nativa, fonte SQL dinâmica e fonte de terceiros. A relação é a seguinte:

insira a descrição da imagem aqui

  • ProviderSqlSource: O terceiro método de fonte SQL, toda vez que você obtém SQL, você cria dinamicamente uma fonte de dados estática com base em parâmetros e, em seguida, cria BoundSql
  • DynamicSqlSource: A fonte SQL dinâmica contém scripts SQL. Toda vez que você obtém SQL, ele cria e cria BoundSql dinamicamente com base em parâmetros e scripts.
  • RawSqlSource: SQL que não contém nenhum elemento dinâmico, texto nativo. Mas este SQL não pode ser executado diretamente, ele precisa ser convertido para BoundSql
  • StaticSqlSource: contém SQL executável e mapeamento de parâmetros, que podem gerar BoundSql diretamente. As três primeiras fontes de dados devem primeiro criar StaticSqlSource e, em seguida, criar BoundSql.

Processo de análise SqlSource

SqlSource é baseado na análise XML. A camada inferior da análise é usar Dom4j para analisar XML em sub-nós. Depois de percorrer esses sub-nós por meio de XMLScriptBuilder, a fonte Sql correspondente é finalmente gerada. Seu processo de análise é o seguinte:

insira a descrição da imagem aqui

Pode-se ver na figura que este é um acesso recursivo a todos os nós, se for um nó de texto, será criado diretamente TextNode ou StaticSqlNode. Caso contrário, nós de script dinâmico, como IfSqlNode, serão criados. Cada tipo de nó dinâmico aqui será criado com um processador correspondente (NodeHandler). Depois de criado, ele continuará visitando os nós filhos, permitindo que a recursão continue. Obviamente, o SqNode criado pelo nó filho também existirá como o nó filho do elemento criado atualmente.

insira a descrição da imagem aqui

Acho que você gosta

Origin blog.csdn.net/zyb18507175502/article/details/131122767
Recomendado
Clasificación