Analysis of MyBatis Dynamic SQL Bottom Principle

When we use mybatis, we write sql statements in xml.

For example, this dynamic sql code:

<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">
    UPDATE users
    <trim prefix="SET" prefixOverrides=",">
        <if test="name != null and name != ''">
            name = #{name}
        </if>
        <if test="age != null and age != ''">
            , age = #{age}
        </if>
        <if test="birthday != null and birthday != ''">
            , birthday = #{birthday}
        </if>
    </trim>
    where id = ${id}
</update>

How does the bottom layer of mybatis construct this sql?

There is not much information on the internet, so I wrote such an article.

Below with this question, we will analyze step by step.

Introduce some interfaces and classes about dynamic SQL in MyBatis

SqlNode interface, a simple understanding is that each tag in xml, such as the sql update,trim,if tag:

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

SqlSource Sql source interface, which represents SQL content mapped from xml files or annotations, is mainly used to create BoundSql. It includes implementation class DynamicSqlSource (dynamic Sql source), StaticSqlSource (static Sql source), etc.

public interface SqlSource {
  BoundSql getBoundSql(Object parameterObject);
}

The BoundSql class encapsulates mybatis and eventually generates sql classes, including sql statements, parameters, parameter source data and other parameters:

XNode, an extension class of Node interface in Dom API.

Base Builder interfaces and their implementation classes (attributes, methods omitted, you are interested to see for yourself), the role of these builders is to construct sql:

Let's briefly analyze four of them:

1 XMLConfigBuilder

Parse the global xml file in the configLocation attribute in mybatis, and use the xml Mapper Builder internally to parse each xml file.

2 XMLMapperBuilder

Traversing through the builder of each node in the XML file in the mapperLocations attribute of mybatis, such as user.xml, the XML Statement Builder is used internally to process each node in the xml.

3 XMLStatementBuilder

Parse the nodes in the xml file, such as select,insert,update,delete nodes. Internally, XMLScript Builder is used to process the sql part of the node. The data generated by traversal is thrown into the mappedStatements of Configuration.

4 XMLScriptBuilder

Parse the Builder of the sql part of each node in xml.

Language Driver interface and its implementation class (attributes, methods omitted, everyone interested in their own view), the main role of the interface is to construct sql:

Simple analysis of XML Language Driver (dealing with SQL in xml, Raw Language Driver dealing with static sql):

Within the xml Language Driver, the sql part of the xml is parsed using the XMLScript Builder.

ok, most of the more important classes we have already introduced, the following source code analysis started.

Source Code Analysis Begins

When integrating Spring with Mybatis, you need to configure SqlSession FactoryBean, which adds information such as data source and mybatis xml configuration file path.

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="configLocation" value="classpath:mybatisConfig.xml"/>
    <property name="mapperLocations" value="classpath*:org/format/dao/*.xml"/>
</bean>

We will analyze the details behind this configuration:

SqlSessionFactory Bean implements Spring's InitializingBean interface, and the buildSqlSessionFactory method is called in the afterPropertiesSet method of InitializingBean interface.

Within the buildSqlSessionFactory method, the XMLConfigBuilder is used to parse the paths configured in the attribute configLocation, and the XMLMapperBuilder property is used to parse the xml files in the mapperLocations attribute.

Some source codes are as follows:

Since XML Config Builder also uses XML Mapper Builder internally, let's look at the parsing details of XML Mapper Builder.

Let's focus on adding, deleting, and modifying the parsing of the checking nodes.

Analysis of XML Statement Builder:

By default, SqlSource (set in the Configuration constructor) is created using XML Language Driver.

XML Language Driver creates SqlSource:

XMLScriptBuilder parses sql:

After getting SqlSource, it will be placed in Configuration. With SqlSource, you can get BoundSql, and BoundSql can get the final sql.

Case study

I will briefly describe the parse DynamicTags parsing process with the following xml parsing:

<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">
    UPDATE users
    <trim prefix="SET" prefixOverrides=",">
        <if test="name != null and name != ''">
            name = #{name}
        </if>
        <if test="age != null and age != ''">
            , age = #{age}
        </if>
        <if test="birthday != null and birthday != ''">
            , birthday = #{birthday}
        </if>
    </trim>
    where id = ${id}
</update>

The return value of the parseDynamicTags method is a List, which is a set of Sql nodes. SqlNode is introduced at the beginning of this article. After analyzing the parsing process, we will talk about the role of each SqlNode type.

Firstly, all the sub-nodes are obtained according to the update node (Node), which are three sub-nodes respectively.

(1) Text noden UPDATE users

(2)trim sub-node ____________.

(3) Text noden where id = {id}

2 traverse each sub-node

(1) If the node type is text or CDATA, construct a TextSqlNode or StaticTextSqlNode

(2) If the node type is an element, the update node is a dynamic sql, and then the NodeHandler is used to process all types of child nodes. NodeHandler is an internal interface of XMLScript Builder. Its implementation classes include TrimHandler, WhereHandler, SetHandler, IfHandler, ChooseHandler, etc. Looking at the class name, we can see the function of this Handler. For example, the trim node we analyzed corresponds to TrimHandler, and the if node corresponds to IfHandler.

Here the sub-node trim is processed by TrimHandler, and parseDynamicTags method is also used to parse the node inside TrimHandler.

3 Repeat the above steps if the child node is an element

There are seven sub-nodes in the trim sub-node, which are text node, if node, text node, if node, text node, if node, if node. Text nodes are processed as before, and if nodes are processed using IfHandler

The traversal steps are shown above. Let's take a look at the implementation details of several handlers.

The IfHandler method also uses the parseDynamicTags method and then adds the necessary attributes of the if tag.

private class IfHandler implements NodeHandler {
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      List<SqlNode> contents = parseDynamicTags(nodeToHandle);
      MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
      String test = nodeToHandle.getStringAttribute("test");
      IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
      targetContents.add(ifSqlNode);
    }
}

The TrimHandler method also uses the parseDynamicTags method and then adds the necessary attributes of the trim tag.

private class TrimHandler implements NodeHandler {
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      List<SqlNode> contents = parseDynamicTags(nodeToHandle);
      MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
      String prefix = nodeToHandle.getStringAttribute("prefix");
      String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
      String suffix = nodeToHandle.getStringAttribute("suffix");
      String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
      TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
      targetContents.add(trim);
    }
}

The SqlNode set obtained by the above update method through the parseDynamicTags method is as follows:

trim node:

Because this update method is a dynamic node, a dynamic SqlSource is constructed.

Within DynamicSqlSource, sql can be constructed:

The SqlNode property inside DynamicSqlSource is a Mixed SqlNode.

Then let's look at the apply methods for each SqlNode implementation class

The following is an analysis of the implementation of two SqlNode implementation classes using the apply method:

MixedSqlNode:

public boolean apply(DynamicContext context) {
    for (SqlNode sqlNode : contents) {
      sqlNode.apply(context);
    }
    return true;
}

Mixed SqlNode traverses the application methods that call each sqlNode inside.

StaticTextSqlNode:

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

Appnd SQL text directly.

IfSqlNode:

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

The evaluator here is an example of the ExpressionEvaluator type, which uses OGNL internally to process expression logic.

TrimSqlNode:

public boolean apply(DynamicContext context) {
    FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
    boolean result = contents.apply(filteredDynamicContext);
    filteredDynamicContext.applyAll();
    return result;
}

public void applyAll() {
  sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
  String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
  if (trimmedUppercaseSql.length() > 0) {
    applyPrefix(sqlBuffer, trimmedUppercaseSql);
    applySuffix(sqlBuffer, trimmedUppercaseSql);
  }
  delegate.appendSql(sqlBuffer.toString());
}

private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
  if (!prefixApplied) {
    prefixApplied = true;
    if (prefixesToOverride != null) {
      for (String toRemove : prefixesToOverride) {
        if (trimmedUppercaseSql.startsWith(toRemove)) {
          sql.delete(0, toRemove.trim().length());
          break;
        }
      }
    }
    if (prefix != null) {
      sql.insert(0, " ");
      sql.insert(0, prefix);
    }
  }
}

TrimSqlNode's apply method is also an apply method that calls attribute contents (usually Mixed SqlNode). According to the example, there are seven SqlNodes, which are StaticTextSqlNode and IfSqlNode. Finally, prefix and suffix are filtered out using Filtered DynamicContext.

summary

This paper gives a brief account of mybatis's parsing process of dynamic sql statements. In fact, it's not too complicated to look back on. It's quite simple. It's always hard to understand the dynamic sql I just analyzed when I came into contact with mybaits.

<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">
    UPDATE users
    <trim prefix="SET" prefixOverrides=",">
        <if test="name != null and name != ''">
            name = #{name}
        </if>
        <if test="age != null and age != ''">
            , age = #{age}
        </if>
        <if test="birthday != null and birthday != ''">
            , birthday = #{birthday}
        </if>
    </trim>
    where id = ${id}
</update>

To figure out what prefix overrides mean for this trim node (literally prefix overrides), there is no official documentation on this knowledge. I changed this xml to read as follows:

<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">
    UPDATE users
    <trim prefix="SET" prefixOverrides=",">
        <if test="name != null and name != ''">
            , name = #{name}
        </if>
        <if test="age != null and age != ''">
            , age = #{age}
        </if>
        <if test="birthday != null and birthday != ''">
            , birthday = #{birthday}
        </if>
    </trim>
    where id = ${id}
</update>

(The first if node in the second paragraph has a comma.) As a result, I found that the results of the two xml parses were the same, and I was eager to know exactly why, which prompted me to look at the source code. Eventually, I saw it.

Keywords: SQL xml Mybatis Attribute

Added by studgate on Thu, 11 Jul 2019 02:29:47 +0300