With Mybatis we use XML to configure our SQL. Here’s how Mybatis parses XML into executable SQL.

Mybatis supports only insert, SELECT, UPDATE, delete and referenced SQL tags, as well as if, choose, when, otherwise, trim, WHERE, SET and foreach dynamic statement tags.

Insert, SELECT, UPDATE and DELETE tags are only used to indicate the behavior of the statement, and do not affect the parsing of THE XML into SQL. When writing XML statements, we also found that we cannot omit the SELECT tag in the < SELECT > tag. The main influence on the generation of SQL statements is the dynamic labels contained in the statements.

Mybatis is divided into two stages: configuration parsing and SQL execution. When SqlSessionFactory is created using build method of SqlSessionFactoryBuilder, it is the configuration parsing stage. During this period, Mybatis will parse all the Configuration in XML and all the statements in Mapper. The result of parsing is stored in the Configuration for execution, and the statement parsing in Mapper takes place at this time.

Each INSERT, SELECT, UPDATE, and DELETE statement is parsed as an MappedStatement and stored in the mappedStatements of the Configuration:

// src/main/java/org/apache/ibatis/session/Configuration.java
public class Configuration {
   protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("")
}
Copy the code

Key indicates the statement ID, that is, namespace. statement ID.

MappedStatement

MappedStatement encapsulates the SQL statement and contains the attributes and SQL of the statement.

// src/main/java/org/apache/ibatis/mapping/MappedStatement.java
public final class MappedStatement {
    / / statement id
    private String id;
    // The amount of results obtained each time
    private Integer fetchSize;
    // Statement timed out
    private Integer timeout;
    // SQL for the statement
    private SqlSource sqlSource;
    // Statement type
    private StatementType statementType;
    // Result type
    privateResultSetType resultSetType; . }Copy the code

SqlSource represents the ‘source’ of the executable SQL, and BoundSql is used to obtain the currently executed BoundSql by calling SqlSource’s getBoundSql method at execution time. BoundSql represents the SQL information that was executed this time. It contains executable SQL, variables referenced in SQL, and parameters given to SQL. SqlSource is an interface that defines only the getBoundSql method:

public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

}
Copy the code

ParameterObject is the parameter we pass when we execute the statement.

The parsing process

When SqlSessionFactory is created using the build method of SqlSessionFactoryBuilder, all XML configuration files are parsed. XML configuration is parsed by XMLConfigBuilder. XMLConfigBuilder parses all mapper references in configuration files. Mapper is parsed by XMLMapperBuilder. XMLMapperBuilder parses statements in mapper one by one. Parsing of individual statements is done by the XMLStatementBuilder, which parses the MappedStatement of the resulting statement and adds it to the Configuration for execution.

SQL tag parsing

1.1 Review of SQL Tag Usage

SQL elements are used to define SQL content that can be reused by other statements. ${} is a static parameter that must be specified in

and replaced when the statement is parsed. ${} is a static parameter that must be specified in

. If not specified in

, the parameter is used as a dynamic parameter of the statement to be dynamically replaced when the statement is executed.


 <sql id="userFields">
     id, age, ${extraField}
 </sql>

<select id="queryUser" resultType="Map">
    select 
    <include refid="userFields" >
        <property name="extraField" value="name"/>
    </include>
    from user where id = ${id}
</select>
Copy the code

Include references and parameters in the SQL are parsed and replaced when the XML is loaded. For this example, queryUser’s include to userFields is replaced when the XML is parsed:

<select id="queryUser" resultType="Map">
    select
    id, age, name
    from user where id = ${id}
</select>
Copy the code

1.2 SQL label implementation

During mapper parsing, all SQL DOM nodes defined in mapper are searched out and stored in sqlFragments on the Configuration for later use. The SQL statement key is ‘namespace.id’.

// src/main/java/org/apache/ibatis/session/Configuration.java
public class Configuration {
  // Store the SQL node in mapper
  protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");
}

// src/main/java/org/apache/ibatis/builder/xml/XMLMapperBuilder.java
// Add SQL nodes to sqlFragments
public class XMLMapperBuilder extends BaseBuilder {
  private void sqlElement(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      // Statement unique ID
      String id = context.getStringAttribute("id");
      id = currentNamespace + "." + id;
      // Add the id if it does not exist
      if(databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) { sqlFragments.put(id, context); }}}}Copy the code

After obtaining all SQL nodes, if the inclue node is encountered during statement parsing, the DOM node of the SQL referenced by the include will be obtained from sqlFragments of the Configuration and copied. This is done with XMLIncludeTransformer:

private Node findSqlFragment(String refid, Properties variables) {
    refid = PropertyParser.parse(refid, variables);
    refid = builderAssistant.applyCurrentNamespace(refid, true);
    try {
      // Get the SQL node
      XNode nodeToInclude = configuration.getSqlFragments().get(refid);
      // Clone a node
      return nodeToInclude.getNode().cloneNode(true);
    } catch(IllegalArgumentException e) { ... }}Copy the code

After the access to the SQL replication nodes will be ${} in a recursive parsing the node of reference, if the reference specified in the include the replacement:

// src/main/java/org/apache/ibatis/builder/xml/XMLIncludeTransformer.java
public class XMLIncludeTransformer {
  private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
		if (source.getNodeName().equals("include")) {... }else else if (source.getNodeType() == Node.ELEMENT_NODE) {
        	if(included && ! variablesContext.isEmpty()) {// The attributes in the children of the SQL node are parsed to replace ${} references
                NamedNodeMap attributes = source.getAttributes();
                for (int i = 0; i < attributes.getLength(); i++) { Node attr = attributes.item(i); attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext)); }}}else if(included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE) && ! variablesContext.isEmpty()) {// SQL node text nodes parse attributes and replace them directlysource.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext)); }}}Copy the code

After the parameter reference is parsed, the include node should be replaced with the referenced SQL node. After the replacement, the original include node will become the referenced SQL node. However, we do not need the tag outside the < SQL >, we need the contents inside, so we need to move the contents outside. Delete the SQL node:

// src/main/java/org/apache/ibatis/builder/xml/XMLIncludeTransformer.java
public class XMLIncludeTransformer {
  private void applyIncludes(Node source, final Properties variablesContext, boolean included) {...// Insert SQL nodes from other files into the current DOM if they are referenced
      if(toInclude.getOwnerDocument() ! = source.getOwnerDocument()) { toInclude = source.getOwnerDocument().importNode(toInclude,true);
      }
      // include is replaced with SQL
      source.getParentNode().replaceChild(toInclude, source);
      // Insert the child node of the replaced SQL node before it
      while (toInclude.hasChildNodes()) {
          toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
      }
      // All children have been inserted, leaving empty SQL nodes to be removedtoInclude.getParentNode().removeChild(toInclude); . }}Copy the code

At this point, the include is all replaced with the specified SQL, and the #{} references in the SQL are also replaced, so the attribute resolution in the SQL tag occurs when the statement is parsed, and we can consider it static.

Second, the SQL tree

You can see that the insert, SELECT, UPDATE, and DELETE statements are configured as a tree structure, where the text within the tag is called text child nodes and the tag itself is called element nodes, and each node is parsed as SqlNode. In addition to the select, UPDATE, INSERT, and DELETE tags of the statement, each subtag in the statement has its own SqlNode, such as IfSqlNode for if, TrimSqlNode for trim, TextSqlNode for text, and so on.

2.1 MixedSqlNode

MixedSqlNode is also a class of SqlNode and represents a node with multiple child nodes. Insert, SELECT, update, delete tags are not useful for statement parsing. After all, we still need to write insert, SELECT, update, delete tags inside the statement. So use MixedSqlNode to hold these child nodes.

Public class MixedSqlNode implements SqlNode {private final List<SqlNode> contents; public MixedSqlNode(List<SqlNode> contents) { this.contents = contents; }}Copy the code

2.2 build tree

XML configuration statements are parsed by XMLLanguageDriver. The XMLLanguageDriver createSqlSource method parses the XML configuration to SqlSource, which is the initial parsed statement, Get executable SQL from it at execution time. XML is parsed through XMLScriptBuilder in the createSqlSource method, and XMLScriptBuilder is where the real work is.

XMLScriptBuilder encapsulates text nodes as TextSqlNode for parsing and interfaces containing ${} references as StaticTextSqlNode, For element nodes, the corresponding NodeHandler is used to parse the SqlNode of the node and add it to the tree.

Start parsing to get the first level of nodes, for a node is an element of the recursive parsing of the child nodes, and eventually form a SqlNode tree.

public class XMLScriptBuilder extends BaseBuilder {

   // Parse the node
    protected MixedSqlNode parseDynamicTags(XNode node) {
        List<SqlNode> contents = new ArrayList<>();
        // Get all level 1 nodes and parse
        NodeList children = node.getNode().getChildNodes();
        for (int i=0; i < children.getLength(); i++) {
            XNode child = node.newXNode(children.item(i));
            if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
                // Text child node
                String data = child.getStringBody("");
                TextSqlNode textSqlNode = new TextSqlNode(data);
                if (textSqlNode.isDynamic()) {
                    // dynamic SQL with ${
                    contents.add(textSqlNode);
                    isDynamic = true;
                } else {
                    contents.add(newStaticTextSqlNode(data)); }}else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
                // Element child node
                String nodeName = child.getNode().getNodeName();
                NodeHandler handler = nodeHandlerMap.get(nodeName);
                if (handler == null) {
                    throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
                }
                // Use its handler to parse parsing
                handler.handleNode(child, contents);
                isDynamic = true; }}return newMixedSqlNode(contents); }}Copy the code

The following statement:

<select id="queryUser" resultType="Map">
  select * from user where 1=1 and
  <if test="id ! = null">
     id = ${id}
  </if>
    limit 1
</select>
Copy the code

The answer is:

Generate SQL statements

After parsing the above statement tree, we get a tree composed of each node, the root node type is MixedSqlNode contains all the level nodes below it, each node contains its own level from the node below, and so on, each node is a subtype of SqlNode.

The SqlNode interface defines only one Apply interface for adding the SQL content of your node to the final SQL.

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

After obtaining SQL, the apply method of the root node is called, and the root node then calls the Apply method of the first-level child nodes contained below it, and the recursive process is carried out successively. Finally, each node will add its own appropriate content to the final SQL according to its own situation.

public class MixedSqlNode implements SqlNode {
  // All level 1 child nodes
  private final List<SqlNode> contents;
  
  public MixedSqlNode(List<SqlNode> contents) {
    this.contents = contents;
  }
  
  @Override
  public boolean apply(DynamicContext context) {
    // Call all child nodes apply in turn
    contents.forEach(node -> node.apply(context));
    return true; }}Copy the code

For example, the following if node:

<if test="id ! = null">
     id = ${id}
</if>
Copy the code

IfSqlNode = IfSqlNode = IfSqlNode = IfSqlNode = IfSqlNode = IfSqlNode = IfSqlNode = IfSqlNode = IfSqlNode = IfSqlNode = IfSqlNode = IfSqlNode

DynamicSqlSource = DynamicSqlSource = DynamicSqlSource = DynamicSqlSource = DynamicSqlSource = DynamicSqlSource = DynamicSqlSource

Four, node,

The key point of generating SQL statements is how each node processes the content under its own node. The concrete implementation of several nodes is listed below.

4.1 StaticTextSqlNode

StaticTextSqlNode is a text node with no ${} reference. Add text directly to SQL when apply.

// src/main/java/org/apache/ibatis/scripting/xmltags/StaticTextSqlNode.java
public class StaticTextSqlNode implements SqlNode {
  // Text content
  private final String text;

  public StaticTextSqlNode(String text) {
    this.text = text;
  }
  
  // Add text directly to SQL
  @Override
  public boolean apply(DynamicContext context) {
    context.appendSql(text);
    return true; }}Copy the code

4.2 IfSqlNode

IfSqlNode encapsulates the if tag and contains the expression in the test property and the children under if, which are saved as MixedSqlNode type. When apply, the Boolean value of the expression is obtained by Ognl parsing the expression in text. If the value is true, the child node apply is called to add the child node to SQL. If the value is false, the child node is not processed, that is, the child node is not added to SQL.

public class IfSqlNode implements SqlNode {
  private final ExpressionEvaluator evaluator;
  / / the test content
  private final String test;
  // If all level 1 child nodes are mixedSQLNodes of type
  private final SqlNode contents;

  public IfSqlNode(SqlNode contents, String test) {
    this.test = test;
    this.contents = contents;
    this.evaluator = new ExpressionEvaluator();
  }

  @Override
  public boolean apply(DynamicContext context) {
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      // If test is true, add child nodes
      contents.apply(context);
      return true;
    }
    return false; }}Copy the code

4.3 ChooseSqlNode

ChooseSqlNode is a wrapper for Choose and contains all when nodes under Choose and the default Otherwise node. The behavior of when is the same as that of if. If test is true, the content of when is taken. Therefore, the when tag is also encapsulated as IfSqlNode.

ChooseSqlNode calls the apply methods of when in the order of when when apply, not the nodes following when if apply is true, and otherwise node apply when both are false.

public class ChooseSqlNode implements SqlNode {
  / / otherwise nodes
  private final SqlNode defaultSqlNode;
  // All when tags are of type IfSqlNode
  private final List<SqlNode> ifSqlNodes;

  public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
    this.ifSqlNodes = ifSqlNodes;
    this.defaultSqlNode = defaultSqlNode;
  }

  @Override
  public boolean apply(DynamicContext context) {
    for (SqlNode sqlNode : ifSqlNodes) {
      // If true, no further calls are made
      if (sqlNode.apply(context)) {
        return true; }}// When otherwise is configured for fals, otherwise applay is called
    if(defaultSqlNode ! =null) {
      defaultSqlNode.apply(context);
      return true;
    }
    return false; }}Copy the code

4.4 ForEachSqlNode

ForEachSqlNode is the wrapper of foreach. ForEachSqlNode defines the collection, Item, index, open, close, and separator properties of foreach, as well as the children of foreach:

public class ForEachSqlNode implements SqlNode {
  / / collection properties
  private final String collectionExpression;
  / / the open attribute
  private final String open;
  / / close property
  private final String close;
  / / the separator attribute
  private final String separator;
  / / item properties
  private final String item;
  / / the index attribute
  private final String index;
  // The node in foreach
  private final SqlNode contents;
}
Copy the code

If the value is an array, an ArrayList is created for the array and put back. If the value is a Map, the entrySet of the Map is put back. Any other type or NULL will throw an exception.

// src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java
public class ForEachSqlNode implements SqlNode {
  public boolean apply(DynamicContext context) {
     // Gets the Iterable value of collectio
     finalIterable<? > iterable = evaluator.evaluateIterable(collectionExpression, bindings);if(! iterable.iterator().hasNext()) {return true;
     }
     boolean first = true; applyOpen(context); }}Copy the code

If open is not null, add open to the SQL statement after retrieving the Iterable of the collection:

// src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java
public class ForEachSqlNode implements SqlNode {
  private void applyOpen(DynamicContext context) {
    if(open ! =null) { context.appendSql(open); }}}Copy the code

For each iteration, #{item} references are replaced with __frch_item_i, the number of arguments starting at 0, and #{index} references are replaced with __frch_index_I, indicating its unique reference. In the context, __frch_ITEM_I and __frch_index_I are added as the element and I of the current iteration. Finally, all nodes under foreach are applied to SQL. If a node is not the first node, separator is added first, and then THE SQL of the current iteration is added.

// src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java
public class ForEachSqlNode implements SqlNode {
  public boolean apply(DynamicContext context) {
     for (Object o : iterable) {
        ...
        if (first || separator == null) {
          // Add an empty string before iteration if separator is not specified on the first node
          context = new PrefixedContext(context, "");
        } else {
          // If separator is not the first object and is specified, add separator before iteration
          context = new PrefixedContext(context, separator);
        }
        // Add __frch_item_i and __frch_index_i to the upper and lower files
        if (o instanceof Map.Entry) {
            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);
        }
        // Replace references to #{item} and #{index} and append the contents of foreach to SQL
        contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
     }
     // Add close at the end of the iterationapplyClose(context); }}Copy the code