[Tearing MyBatis source code by hand] Analysis of the whole process of dynamic SQL

Dynamic SQL Overview

Dynamic SQL is one of the powerful functions of MyBatis. It eliminates the trouble of assembling SQL strings in JAVA code, and at the same time retains our independent control over SQL, which is more convenient for SQL performance optimization and transformation.

In dynamic SQL, we use XML script elements to control the assembly of SQL. These are elements used in daily development. Let’s review them together.

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

insert image description here

if

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

Accept an OGNL logical expression through test in the if element, which can be used for conventional logical calculations such as: empty judgment, size, and, or, and calculations for sub-attributes.

choose(when、otherwise)

choose is used to select one of multiple conditions, and if none is met, use the value in otherwise. Similar to switch in java. Of course, this kind of logic can also be realized with if, but the logic expression is relatively complicated. Also, there is no corresponding else element in the if element.

trim(where、set)

trim is used to solve the problem of extra SQL statements after assembling SQL, as in the following example:

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

If the if condition is satisfied, a SQL will be generated finally, with an additional AND character in the grammar

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

If it is not satisfied, the SQL will also be wrong, and there is an extra WHERE in the syntax

  SELECT * FROM BLOG  WHERE 

The where element will only insert a "WHERE" clause if the child element returns nothing. Also, if clauses begin with "AND" or "OR", the where element strips them out as well. where element is equivalent to the following trim element

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

The Set element is used for the extra comma problem when modifying multiple fields, which is equivalent to the following trim element

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

foreach

This element is used to traverse the collection value, such as constructing multiple conditions of in, or adding and modifying batch processing. It allows you to specify a collection, declaring collection item (item) and index (index) variables that can be used in the element body.

OGNL expression

The full name of OGNL is Object Graph Navigation Language (Object Graph Navigation Language), which is a JAVA expression language, which is used by MyBatis to realize the function of dynamic SQL.

The main function of OGNL is to obtain or judge the value according to the properties of the object. It can easily access the value inside the object according to the path of the object property.

The main functions that OGNL can realize are:

  1. Access object properties: user.name
  2. Call method: user.getName()
  3. Access collection elements: user.orders[0]
  4. Determine whether an attribute or method exists: user.name?
  5. Call a static class method or property: @java.lang.Math@PI
  6. Implement comparison and arithmetic operations: user.age > 18
  7. Realize logical operation: user.name && user.age < 30

So OGNL provides us with an easy way to manipulate and judge the value of object properties. We can "."easily implement various operations on object properties by means of.

MyBatis uses OGNL to implement various functions of dynamic SQL:

  1. if: Determine whether the result of the given OGNL expression is true, and if it is true, add the content of the current element.
    <if test="name != null">
      name = #{name}  
    </if>
    
  2. choose, when, otherwise: equivalent to the switch statement in Java, which element content to include is determined according to whether the OGNL expression in the first when is satisfied.
    <choose>
      <when test="age > 18">
        age > 18
      </when>
      <when test="age == 18">
        age == 18 
      </when>
      <otherwise> 
        age < 18
      </otherwise>
    </choose> 
    
  3. trim: used to process the prefix or suffix of the string, where prefixOverrides and suffixOverrides support OGNL expressions.
    <trim prefix="(" prefixOverrides="(" suffix=")" suffixOverrides=")">...</trim>
    
  4. where: The where conditional statement used for dynamically splicing sql will automatically remove the first and or or. Among them, test supports OGNL expressions.
    <where>
      <if test="name != null"> name = #{name} </if>  
      <if test="age != null"> and age = #{age} </if>
    </where>
    
  5. set: The set conditional statement used for dynamically splicing sql will automatically remove the last comma. Among them, test supports OGNL expressions.
    <set>
      <if test="name != null"> name = #{name}, </if>
      <if test="age != null"> age = #{age} </if> 
    </set> 
    
  6. forEach: used to iterate the collection, it will iterate over each element in the collection and execute the tag body content
    <forEach items="${lists}" index="index" item="item" open="(" close=")" separator=",">
    #{item}  
    </forEach>
    

It can be seen that OGNL provides strong support for the dynamic SQL implementation of MyBatis. Through a simple OGNL expression, we can judge whether a given condition is met, and decide whether to include a certain sql fragment, which brings great flexibility to the dynamic SQL function of MyBatis.

Therefore, OGNL is the cornerstone of MyBatis dynamic SQL implementation. It allows MyBatis to filter and join the required SQL statements according to conditions, not just simple splicing, which makes the SQL statements generated by MyBatis extremely flexible and powerful.

At the same time, we also introduce a tool class:ExpressionEvaluator

Therefore, ExpressionEvaluator mainly provides two functions:

  1. Parse OGNL expression: Parse the expression string into an Expression object, which represents the internal semantic structure of the expression.
  2. Evaluate an OGNL expression: Use the Expression object and the specified object graph context to evaluate the final result of the expression.

It mainly provides the following APIs:

  • parseExpression(String expression): Parse the OGNL expression and return Expression.
  • evaluate(String expression, Object root): directly parse and evaluate the expression, the root object is root, and return the result.
  • evaluate(Expression expr, Object root): Use the Expression object expr and the root object root to evaluate the expression and return the result.

Through these APIs, MyBatis can parse the OGNL expression in Mapper.xml into Expression, and then use the parameter object to evaluate the Expression to get the final result, and decide the next action accordingly. This enables MyBatis to support OGNL expressions very clearly and flexibly, and realize the functions of dynamic SQL and lazy loading.

BoundSql

When we looked at the source code, we often saw BoundSql, so what is it?
insert image description here

BoundSql is not directly executable SQL statement . It is a POJO class that encapsulates information about SQL statements.

When we call the method in Mapper, MyBatis will parse the corresponding Mapper.xml file, and generate the final SQL statement to be executed according to the parameters we pass in and the conditions of dynamic SQL. This SQL statement will be encapsulated in the BoundSql object.

The information contained in BoundSql is:

  1. sql: the generated SQL statement itself.
  2. parameterMappings: parameter mapping information in the sql statement. For example, #{name} will be mapped to name, etc.
  3. parameters: Map of incoming parameter values, the key is the parameter name, and the value is the parameter value.
    Then MyBatis will use the MappedStatement object in Configuration to execute this SQL, and MappedStatement also contains BoundSql.

So what is the relationship between MappedStatement and BoundSql?

MappedStatement and BoundSql have a close relationship. simply put:

  • MappedStatement represents a query or updated definition information in Mapper.xml. It contains the ID of the SQL statement, parameter mapping and other information.
  • BoundSql represents the details of a SQL statement to be executed. It contains the actual SQL statement, parameter mapping and parameter values.

When we call the method of the Mapper interface, MyBatis will find the corresponding MappedStatement according to the method signature. Then it will parse the SQL statement in the MappedStatement, and generate the final SQL to be executed according to the parameters we pass in, and this SQL will be wrapped in the BoundSql object.

so,A MappedStatement will correspond to multiple BoundSql, depending on the different parameters we pass in each call. But the same call will only generate one BoundSql

MappedStatement can be understood as the static definition of a statement by MyBatis, while BoundSql is the specific statement information dynamically generated each time it is executed .

Their relationship can be expressed as:

  • A MappedStatement corresponds to a query or update definition in Mapper.xml.
  • Each time the Mapper interface method is called, a BoundSql will be generated.
  • This BoundSql contains the final SQL statement to be executed and parameter information generated according to the MappedStatement and incoming parameters.
  • MyBatis will use MappedStatement and this BoundSql to execute SQL and return the result.

Dynamic SQL main process analysis

insert image description here

SqlNode

SqlNode is an interface used to represent fragment SQL in MyBatis.

Usually, the text of each line of SQL in each insert/update/delete/select tag in Mapper.xml that we write based on the MyBaits framework (including include tags replaced with SQL) is abstracted into SqlNode.

insert image description here

Each dynamic element will have a corresponding script class. For example, if corresponds to ifSqlNode, forEarch corresponds to ForEachSqlNode, and so on. :

  • StaticTextSqlNode: pure SQL statement, does not contain any dynamic SQL statement, for example:select * from user
  • TextSqlNode: SQL statements contain ${} placeholders, for exampleselect * from ${user}
  • IfSqlNode: the SQL statement in the if/when subtag;
  • ChooseSqlNode: the SQL statement in the choose subtag;
  • ForEachSqlNode: the SQL statement in the foreach subtag;
  • VarDecSqlNode: the SQL statement in the bind subtag;
  • TrimSqlNode: the SQL statement in the trim subtag;
  • WhereSqlNode: the SQL statement in the where subtag;
  • SetSqlNode: the SQL statement in the set subtag;
  • MixedSqlNode: If the SQL text of the insert/update/delete/select tag is more than one line, then assemble all SqlNodes together.

insert image description here

The SqlNode interface only defines a boolean apply(DynamicContext context) method, which assembles each SqlNode into a complete SQL statement through the DynamicContext object.

Each part of SQL is a SqlNode, they will be included or excluded according to the judgment of the conditions, and finally combined into a complete SQL statement

These kinds of SqlNodes can be nested with each other to form a SqlNode tree. MyBatis will generate the final SQL statement based on this tree and parameter values

DynamicContext

DynamicContext is like a string of bamboo sticks, and SqlNode is a piece of meat on a bamboo stick. All the meat on a bamboo stick is MixedSqlNode. The meat is strung together through bamboo sticks to form a delicious barbecue——SQL! !

How come there are no condiments in barbecue, just like why SQL statements lack parameters? Parameters are stored in the bindings field in DynamicContext. Get StringJoiner concatenated SQL statement through getSql() method.

insert image description here

  • PARAMETER_OBJECT_KEY: Represents the Key of the parameter object, which MyBatis will use to obtain the parameter object passed in at compile time from the context.
  • DATABASE_ID_KEY: indicates the Key of the database identification, and MyBatis will use this Key to obtain the identification of the current database from the context, such as "MySQL".
  • ContextMap: represents the context Map, and MyBatis will save various data related to the current context in this Map, such as parameter objects, database identifiers, etc.
  • StringJoiner: Used to splice and format SQL fragments in WHERE clauses or SET clauses, which makes the generated SQL neater.
  • uniqueNumber: Represents an integer, which is used to generate the alias of the result set when traversing the collection to prevent SQL syntax errors.

Source code analysis

StaticTextSqlNode

Since it does not contain any dynamic SQL, it does not rely on actual parameters to splice SQL statements

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

insert image description here

Source code of 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;
  }

}

The source code of StaticTextSqlNode is very simple, that is, the SQL statement is spliced ​​behind the previous SQL statement through the appendSql() method of DynamicContext.

TextSqlNode

Since the SQL statement contains ${} placeholders, parameters are required to parse the placeholders.

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

insert image description here

Source code of 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;
	}

insert image description here

You can see that our parameters are obtained from a map in DynamicContext

This GenericTokenParser is a static inner class of TextSqlNode
insert image description here

IfSqlNode

The SQL statement in the if/when sub-tag is abstract, and the SQL statement in the if tag is spliced ​​only when the test expression in the if tag is true.

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

insert image description here

Source code of IfSqlNode:
insert image description here

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

ChooseSqlNode

The SQL statement in the choose sub-tag is abstract. When the test expression in the when tag is established, the SQL statement in it will be spliced, otherwise the SQL statement in the otherwise tag will be used. Similar to the if...else if...else statement in Java, only one branch logic is executed.

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

Source code of 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;
	}

ForEachSqlNode

The SQL abstraction in the foreach sub-tag can obtain the corresponding value through the variables set in the item and index in the tag. index is the index value of the array and collection and the Map type is the value in the key, item is the element in the array and collection and the Map type is the value in the value.

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

Source code of 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;
	}

The remaining SqlNode will not be analyzed, they are all similar, and the effect is achieved by wrapping DynamicContext

Dynamic script structure

There is a nested relationship between scripts. For example, the if element will contain a MixedSqlNode, and the MixedSqlNode will contain 1 to 1 or more other nodes. Finally, a script syntax tree is formed. As shown below, the SQL elements on the left form the syntax tree on the right.At the bottom of the node must be a StaticTextNode or TextNode

insert image description here

Dynamic script execution

The interface of SqlNode is very simple, there is only one apply method, the function of the method is to execute the logic of the current script node, and apply the result to the DynamicContext.

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

For example, when executing apply in IfSqlNode, the If logic is calculated first, and if it passes, it will continue to visit its child nodes. Add the SQL text to the DynamicContext until the TextNode is finally accessed. Through this similar recursive method, the Context will visit all the nodes, and append the final qualified SQL text to the Context.

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

insert image description here
After visiting all the nodes, a SQL string will be generated, but this is not a directly executable SQL, because the parameters inside are still in the form of expressions #{name=name}, you need to use SqlSourceBuilder to build executable SQL and parameter mapping ParameterMapping. Only then can BoundSql be generated. The figure below shows that BoundSql is generated after all nodes are executed in the context.

insert image description here
Looking at the source code, during the process from dynamic SQL to BoundSql, there was a StaticSqlSource generation in the middle? Why do you want to do this, and where is the SqlNode set parsed from XML stored? There is also a new concept SqlSource SQL source here.

SqlSource

SqlNode connects SQL between SqlNodes through DynamicContext. In fact, this function is accomplished through SqlSource. Get BoundSql through the getBoundSql() method of the SqlSource interface, and BoundSql contains complete SQL statements, parameter lists and actual parameters. SqlSource represents the content of a mapped statement read from an XML file or annotation. The SQL it creates will be passed to the database based on the input parameters received from the user.

What is the relationship between SqlSource and SqlNode?

  • In MyBatis, both SqlSource and SqlNode are classes used to encapsulate SQL statements.
  • SqlNode is MyStaticTextSqlNode, IfSqlNode, WhereSqlNode, etc., and each SqlNode will process a SQL fragment. These SqlNodes can be combined to form a tree structure of SQL statements.
  • SqlSource stitches the tree structure of SQL statements into a complete SQL statement and provides parameter mapping function. SqlSource is usually composed of SqlNode. By parsing the tree structure of SQL statements in the Mapper.xml file, the final spliced ​​SQL is passed to the JDBC executor.
  • therefore,SqlNode is an intermediate product that generates SqlSource. SqlNode forms a tree-structured SQL statement by parsing the Mapper.xml file, and SqlSource splices these SQL statements into a complete SQL, performs parameter mapping, and finally delivers it to the JDBC executor for execution.

SqlSource has the following categories:

  • DynamicSqlSource: SQL for 动态 SQLand ${}placeholders
  • RawSqlSource: #{}SQL for placeholders
  • ProviderSqlSource: @*ProviderSQL provided for annotations
  • StaticSqlSource: contains only ?SQL with placeholders

When Mybatis processes #{}, it will replace #{} in sql with a ? number, and call the set method of PreparedStatement to assign a value; when
Mybatis processes ${}, it will replace ${}it with the value of the variable.

insert image description here

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

Run the console output of the above program SELECT * FROM user WHERE id = #{id}without any processing.

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

}

It can be seen from the StaticSqlSource#getBoundSql method that the original SQL statement will not be processed when the BoundSql object is obtained.

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

Run the above program console outputSELECT * 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;
  }

}

The SqlNode is processed when the BoundSql object is obtained through the DynamicSqlSouce#getBoundSql() method. If it is dynamic SQL and the SQL statement that means ${} placeholder, splice and replace according to the incoming actual parameters. If it is #{} placeholder? Replace, and finally build BoundSql through 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());
    }
}

Run the console output of the above program SELECT * FROM user WHERE id = ?, what will happen if the #{} placeholder is cached for the ${} placeholder or the SqlNode is replaced with another dynamic SqlNode?

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

}

The questions raised in the examples can be answered through source code analysis. If it is a ${} placeholder, it will not be processed, and the dynamic SQL may report an error or the processed SQL statement may be incomplete.

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

Run the above program console outputSELECT * 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);
	}

To sum up:
parse the SQL according to the SQL source to obtain the SqlNode, and obtain the corresponding SqlSource according to the SqlNode, whether it is DynamicSqlSource or RawSqlSource. If it is DynamicSqlSource splicing dynamic SQL and processing ${} placeholders according to actual parameters, and then converting to StaticSqlSource through SqlSourceBuilder#parse() method, and RawSqlSource has been converted to StaticSqlSource through SqlSourceBuilder#parse() method when instantiated, which does not depend on actual parameters, so the performance is faster than DynamicSqlSource . ProviderSqlSource obtains DynamicSqlSource or RawSqlSource through XMLLanguageDriver#createSqlSource() method after parsing the SQL statement.

Summarize

In the upper definition, each Sql mapping (MappedStatement) will contain a SqlSource to obtain executable Sql (BoundSql). SqlSource is divided into native SQL source, dynamic SQL source, and third-party source. The relationship is as follows:

insert image description here

  • ProviderSqlSource: The third method SQL source, every time you get SQL, you will dynamically create a static data source based on parameters, and then create BoundSql
  • DynamicSqlSource: The dynamic SQL source contains SQL scripts. Every time SQL is obtained, it will dynamically create and create BoundSql based on parameters and scripts.
  • RawSqlSource: SQL that does not contain any dynamic elements, native text. But this SQL cannot be executed directly, it needs to be converted to BoundSql
  • StaticSqlSource: Contains executable SQL and parameter mapping, which can directly generate BoundSql. The first three data sources must first create StaticSqlSource and then create BoundSql.

SqlSource parsing process

SqlSource is based on XML parsing. The bottom layer of parsing is to use Dom4j to parse XML into sub-nodes. After traversing these sub-nodes through XMLScriptBuilder, the corresponding Sql source is finally generated. Its analysis process is as follows:

insert image description here

It can be seen from the figure that this is a recursive access to all nodes. If it is a text node, TextNode or StaticSqlNode will be created directly. Otherwise, dynamic script nodes such as IfSqlNode will be created. Each type of dynamic node here will be created with a corresponding processor (NodeHandler). After it is created, it will continue to visit the child nodes, allowing the recursion to continue. Of course, the SqNode created by the child node will also exist as the child node of the currently created element.

insert image description here

Guess you like

Origin blog.csdn.net/zyb18507175502/article/details/131122767