Mybatis源码深度解析之SQL语句生成

2,805 阅读8分钟

使用mybatis时我们使用xml来配置我们的sql,这里我们来看一下mybatis是如何将xml解析为可执行的sql的。

mybatis支持的语句并不多只有insert、select、update、delete和被引用的sql五个标签,以及if、choose、when、otherwise、trim、where、set和foreach八个动态语句标签。

其中insert、select、update、delete这四个语句标签只是标示语句的行为,并不会影响到其中的xml解析为sql,在写xml语句时我们也发现在<select>标签中我们也不能省略select。影响生成sql语句的主要还是语句中包含的动态标签。

mybatis分为解析配置和执行sql两个阶段,在使用SqlSessionFactoryBuilder的build方法创建SqlSessionFactory时为配置解析阶段,这期间mybatis会解析xml中的所有的配置和mapper中的所有语句,解析得到的结果存放在Configuration中供执行时使用,mapper中的语句解析便发生在这个时候。

每个insert、select、update、delete语句被解析为MappedStatement存放在Configuration的mappedStatements中:

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

key为语句的id,也就是"namespace.语句id"。

MappedStatement

MappedStatement为sql语句的封装,包含了语句中的各个属性和语句的sql。

// src/main/java/org/apache/ibatis/mapping/MappedStatement.java
public final class MappedStatement {
    // 语句id
    private String id;
    // 每次获取结果的量
    private Integer fetchSize;
    // 语句超时
    private Integer timeout;
    // 语句的sql
    private SqlSource sqlSource;
    // 语句类型
    private StatementType statementType;
    // 结果类型
    private ResultSetType resultSetType;
    ...

}

SqlSource代表可执行sql的‘源’,在执行时通过调用SqlSource的getBoundSql方法来获取当前执行的BoundSql,而BoundSql就代表这一次执行的sql信息,其中包含了可执行sql、sql中引用变量和给到sql的参数。SqlSource是一个接口只定义了getBoundSql这一个方法:

public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

}

parameterObject是我们执行语句时传递的参数。

解析过程

在使用SqlSessionFactoryBuilder的build方法创建SqlSessionFactory时会解析所有的xml配置文件,xml配置由XMLConfigBuilder负责解析,在XMLConfigBuilder会解析配置文件中引用的所有mapper,mapper由XMLMapperBuilder进行解析,XMLMapperBuilder在解析时会对Mapper中的语句逐个进行解析,单个语句的解析由XMLStatementBuilder进行,XMLStatementBuilder解析得到语句的MappedStatement并将其添加到configuration中供执行时使用。

一、sql标签解析

1.1 sql标签使用回顾

sql元素用来定义可被其它语句重复使用的sql内容。sql中可以使用${}定义参数,但与语句中的参数引用不同,sql中的{}参数引用不同,sql中的{}参数为静态参数,需要在<include>中指定并在解析语句时被替换,若<include>中未指定则参数作为语句的一个动态参数在执行语句时去动态替换。

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

include的引用和sql中的参数在加载xml时被解析替换,对于这个例子在解析xml时queryUser对userFields的include就会被替换为:

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

1.2 sql标签实现

在解析mapper时首先会将mapper中定义的所有sql的dom节点查找出来,然后存放在Configuration的sqlFragments中供后面使用,sql的语句key为'namespace.id'。

// src/main/java/org/apache/ibatis/session/Configuration.java
public class Configuration {
  // 存放mapper中的sql节点
  protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");
}

// src/main/java/org/apache/ibatis/builder/xml/XMLMapperBuilder.java
// 将sql节点添加到sqlFragments中
public class XMLMapperBuilder extends BaseBuilder {
  private void sqlElement(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      // 语句唯一id
      String id = context.getStringAttribute("id");
      id = currentNamespace + "." + id;
      // id不存在则添加
      if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
        sqlFragments.put(id, context);
      }
    }
  }
}

在获取到所有的sql节点后,在解析语句时若遇到inclue的节点,则会从Configuration的sqlFragments中获取到include引用的sql的dom节点并复制一个。该操作由XMLIncludeTransformer进行:

private Node findSqlFragment(String refid, Properties variables) {
    refid = PropertyParser.parse(refid, variables);
    refid = builderAssistant.applyCurrentNamespace(refid, true);
    try {
      // 获取sql节点
      XNode nodeToInclude = configuration.getSqlFragments().get(refid);
      // 克隆一个节点
      return nodeToInclude.getNode().cloneNode(true);
    } catch (IllegalArgumentException e) {
      ...
    }
  }

获取到sql的复制节点后,会递归的解析这个节点中的${}的引用,若引用在include中有指定则替换:

// 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()) {
                // sql节点的子节点中的属性要解析替换${}引用
                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节点文本节点则直接解析属性并替换
            source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
        }
  }
}

在解析完参数引用之后要将include节点替换为引用的sql节点,在替换完成后原来的include节点就变为了引用的sql节点,但外面<sql>的这个标签我们并不需要,我们需要的是其里面的内容,所以需要将里面的内容移到外面来,并将sql节点删除:

// src/main/java/org/apache/ibatis/builder/xml/XMLIncludeTransformer.java
public class XMLIncludeTransformer {
  private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
      ...
      // 若引用的是其它文件中的sql节点,则将其它文件中的sql节点插入到当前dom中
      if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
          toInclude = source.getOwnerDocument().importNode(toInclude, true);
      }
      // include替换为sql
      source.getParentNode().replaceChild(toInclude, source);
      // 将替换的sql节点的子节点插入到其之前
      while (toInclude.hasChildNodes()) {
          toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
      }
      // 其子节点已经全部插入,剩下空sql节点移除
      toInclude.getParentNode().removeChild(toInclude);
      ...
  }
}

至此include就全部替换为了指定的sql,并且sql中的#{}引用也被替换,所以sql标签中属性解析发生在语句解析时,我们可以认为它时静态的。

二、SQL树

可以看出我们配置的insert、select、update、delete语句的内容是一个树形结构,其中标签内的文本称为文本子节点而标签自身称为元素节点,在解析时各个节点被解析为SqlNode。除语句的select、update、insert、delete标签,语句里面的每个子标签都有自己的SqlNode,如if的IfSqlNode、trim的TrimSqlNode、文本的TextSqlNode等。

2.1 MixedSqlNode

MixedSqlNode也是SqlNode的一类,它表示一个包含多个子节点的节点。比如insert、select、update、delete这些外层的标签对于语句的解析并没有用,毕竟写了这些标签我们还在要在里面写上insert、select、update、delete,真正需要解析的时其里面的自节点,所以使用MixedSqlNode持有这些子节点。

public class MixedSqlNode implements SqlNode {
  // 自节点
  private final List<SqlNode> contents;

  public MixedSqlNode(List<SqlNode> contents) {
    this.contents = contents;
  }
}

2.2 构建树

xml配置的语句由XMLLanguageDriver进行解析,XMLLanguageDriver的createSqlSource方法会将xml配置解析为SqlSource,SqlSource是初步解析后的语句,在执行时从其中获取可执行的sql。createSqlSource方法中通过XMLScriptBuilder来解析xml,XMLScriptBuilder才是真正干活的地方。

XMLScriptBuilder在解析时对于文本节点封装为TextSqlNode对于包含${}引用的接口封装为StaticTextSqlNode,对于元素节点则使用元素对应的NodeHandler进行解析得到其节点的SqlNode并添加到树中。

开始解析时先获取一级节点,对于是一个元素的一节点则递归的先解析其子节点,最终形成一个SqlNode的树。

public class XMLScriptBuilder extends BaseBuilder {

   // 解析节点
    protected MixedSqlNode parseDynamicTags(XNode node) {
        List<SqlNode> contents = new ArrayList<>();
        // 获取所有一级节点并解析
        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) {
                // 文本子节点
                String data = child.getStringBody("");
                TextSqlNode textSqlNode = new TextSqlNode(data);
                if (textSqlNode.isDynamic()) {
                    // 若含有${}则为动态sql
                    contents.add(textSqlNode);
                    isDynamic = true;
                } else {
                    contents.add(new StaticTextSqlNode(data));
                }
            } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
                // 元素子节点
                String nodeName = child.getNode().getNodeName();
                NodeHandler handler = nodeHandlerMap.get(nodeName);
                if (handler == null) {
                    throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
                }
                // 使用其handler解析解析
                handler.handleNode(child, contents);
                isDynamic = true;
            }
        }
        return new MixedSqlNode(contents);
    }
}

如下面的语句:

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

解析后就为:

三、生成sql语句

在完上面的语句树解析后就得到了由各个节点组成的树,其根节点类型为MixedSqlNode包含了其下面的所有一级节点,下面各个节点又包含了自己下面的一级自节点,依次类推,每个节点都是SqlNode的子类型。

SqlNode接口只定义了一个apply接口,用于将自己节点的sql内容添加到最终的sql上。

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

在获取sql调用根节点的apply方法,根节点再调用其下面包含的一级子节点的apply方法,依次递归的进行,最终各个节点依次自己的情况将自己合适的内容添加到最终的sql上。

public class MixedSqlNode implements SqlNode {
  // 所有的一级子节点
  private final List<SqlNode> contents;
  
  public MixedSqlNode(List<SqlNode> contents) {
    this.contents = contents;
  }
  
  @Override
  public boolean apply(DynamicContext context) {
    // 依次调用所有子节点apply
    contents.forEach(node -> node.apply(context));
    return true;
  }
}

比如下面这个if节点:

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

这个if的节点为IfSqlNode,在获取语句时会调用其apply方法,在IfSqlNode的apply中会判断test是否为true若为true则将if中所有内容,这里为‘id = ${id}’追加到语句中。

SqlSource有DynamicSqlSource和StaticSqlSource两种类型的,若一个语句中包含${}的引用或其它元素的子节点则语句封装为DynamicSqlSource否则为StaticSqlSource。

四、节点

生成sql语句关键点还是在于各个节点是如何处理自己节点下的内容,下面列举了几个节点的具体实现。

4.1 StaticTextSqlNode

StaticTextSqlNode是不含${}引用的文本节点。在apply时直接将文本添加到sql中。

// src/main/java/org/apache/ibatis/scripting/xmltags/StaticTextSqlNode.java
public class StaticTextSqlNode implements SqlNode {
  // 文本内容
  private final String text;

  public StaticTextSqlNode(String text) {
    this.text = text;
  }
  
  // 将文本直接添加到sql中
  @Override
  public boolean apply(DynamicContext context) {
    context.appendSql(text);
    return true;
  }
}

4.2 IfSqlNode

IfSqlNode为if标签的封装,包含test属性中的表达式和if下的子节点,这些子节点保存为MixedSqlNode类型。在apply时通过Ognl解析text中的表达式获并获取表达式的boolean值,若值为true则调用子节点apply将子自节点添加到sql中,若为false则不处理子节点也就是不将子节点添加到sql中。

public class IfSqlNode implements SqlNode {
  private final ExpressionEvaluator evaluator;
  // test内容
  private final String test;
  // if下的所有一级子节点类型为MixedSqlNode
  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())) {
      // test为true则添加子节点
      contents.apply(context);
      return true;
    }
    return false;
  }
}

4.3 ChooseSqlNode

ChooseSqlNode为choose的封装,包含了choose下的所有when节点和默认的otherwise节点。其中when的行为与if是相同的,都是test为true则取when中的内容,所以when标签也被封装为IfSqlNode。

ChooseSqlNode在apply时按按照when的顺序来调用when的apply方法,若apply为true则不在调用后面的when节点,当when都为false则调用otherwise节点的apply。

public class ChooseSqlNode implements SqlNode {
  //otherwise节点
  private final SqlNode defaultSqlNode;
  // 所以的when标签,类型为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) {
      // 为true则不在进行后面的调用
      if (sqlNode.apply(context)) {
        return true;
      }
    }
    // when都为fals配置了otherwise则调用otherwise的applay
    if (defaultSqlNode != null) {
      defaultSqlNode.apply(context);
      return true;
    }
    return false;
  }
}

4.4 ForEachSqlNode

ForEachSqlNode为foreach的封装,ForEachSqlNode中定义了foreach的collection、item、index、open、close和separator属性以及foreach中的子节点:

public class ForEachSqlNode implements SqlNode {
  // collection属性
  private final String collectionExpression;
  // open属性
  private final String open;
  // close属性
  private final String close;
  // separator属性
  private final String separator;
  // item属性
  private final String item;
  // index属性
  private final String index;
  // foreach中的节点
  private final SqlNode contents;
}

在apply时,先使用ognl解析collection属性并获取表达式的值,若值是Iterable类型的则可以直接迭代访问,若值为数组则为数组创建一个ArrayList并放回,若值为Map则放回Map的entrySet,其它类型或null都将抛出异常。

// src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java
public class ForEachSqlNode implements SqlNode {
  public boolean apply(DynamicContext context) {
     // 获取collectio的Iterable值
     final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
     if (!iterable.iterator().hasNext()) {
       return true;
     }
     boolean first = true;
     applyOpen(context);
  }
}

在获取到collection的Iterable的值后,若open不为null则先将open添加到sql语句中:

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

之后开始对Iterable进行遍历,对于每个迭代会将#{item}的引用替换为__frch_item_i,i为第几个参数从0开始,#{index}的引用替换为__frch_index_i,依次来标示其唯一引用,并在上下文中添加__frch_item_i和__frch_index_i值为当前迭代的元素和i,最后将foreach下所有节点apply到sql中,如果一个节点非第一个节点则会先添加separator,然后在添加当前迭代的sql。

// 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) {
          // 若时第一个节点或未指定separator则在迭代前添加空字符串
          context = new PrefixedContext(context, "");
        } else {
          // 非第一个节点且指定了separator则在迭代前添加separator
          context = new PrefixedContext(context, separator);
        }
        // 上下文件中添加__frch_item_i和__frch_index_i
        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);
        }
        // 替换#{item}和#{index}的引用并在sql中追加foreach中的内容
        contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
     }
     // 迭代结束添加close
     applyClose(context);
  }
}