Mybatis源码分析(四):mapper.xml增删查改的节点解析和动态SQL的实现

尹小云
2023-12-01

概述

  • 我们通常在mapper.xml中定义增删查改select|insert|update|delete的相关SQL,在对应的select|insert|update|delete节点中支持通过if,(choose, when, otherwise),(trim, where, set),foreach等内嵌节点来实现动态SQL定义,动态SQL的使用方法可以参考:动态 SQL
  • 对于增删查改,在对应的select|insert|update|delete节点中,通过id属性来映射mapper接口的对应方法,其中mapper接口是通过父节点mapper的namespace来定义映射的。
  • 由上篇文章:Mybatis源码分析(三):mapper.xml的解析及namespace与Mapper接口的映射
    的分析可知,mapper.xml主要是通过builder包的xml子包的XMLMapperBuilder来解析,对应增删查改select|insert|update|delete节点的解析主要是在buildStatementFromContext方法中定义的。

增删查改节点解析:XMLStatementBuilder

  • buildStatementFromContext方法的定义如下:其中参数list为select|insert|update|delete对应的类型为XNode的节点集合。

    private void buildStatementFromContext(List<XNode> list) {
        if (configuration.getDatabaseId() != null) {
          buildStatementFromContext(list, configuration.getDatabaseId());
        }
        buildStatementFromContext(list, null);
    }
    
    private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
        // list为mapper.xml文件中SQL操作(select|insert|update|delete)节点列表
        for (XNode context : list) {
        
          // 增删查改节点的解析器
          final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
          try {
            // 解析mapper.xml文件中的相关SQL操作标签,
            // 并存放到configuration的mappedStatements集合中
            statementParser.parseStatementNode();
          } catch (IncompleteElementException e) {
            // 如果存在解析异常,则放到incompleteStatements中
            configuration.addIncompleteStatement(statementParser);
          }
        }
    }
    
  • 在内部增删查改节点的解析主要是通过builder包的xml子包的XMLStatementBuilder来完成的,具体在XMLStatementBuilder的parseStatementNode方法中定义解析逻辑:

    public void parseStatementNode() {
        // mapper接口的方法名
        String id = context.getStringAttribute("id"); 
        
        // fetchSize,parameterMap,parameterType,resultType,resultMap相关属性的解析
        ...
    
        // flushCache,useCache缓存相关的属性的解析
        ...
    
        // include节点解析,主要用于include相关的SQL片段,如需要select的列
        ...
        
        // 解析SQL,使用SqlSource存放
        SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    
        // keyGenerator相关
        ...
        
        // 构造当前增删查改节点对应的MappedStatement对象
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
            resultSetTypeEnum, flushCache, useCache, resultOrdered, 
            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
    }
    
  • 在parseStatementNode中,在解析好select|insert|update|delete节点相关的属性之后,最后通过builder包的MapperBuilderAssistant来创建对应的MappedStatement对象,然后保存该MappedStatement对象到Configuration的mappedStatements集合中。其中SqlSource是用来保存SQL节点的类,包括动态SQL节点。

    public MappedStatement addMappedStatement(...参数列表) {
    
        if (unresolvedCacheRef) {
          throw new IncompleteElementException("Cache-ref not yet resolved");
        }
    
        id = applyCurrentNamespace(id, false);
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    
        // MappedStatement构建器,组装select|insert|update|delete节点的属性值,构造SQL对应的MappedStatement对象
        // 然后将该mappedStatement对象实例保存到Configuration的mappedStatements集合
        MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
            .resource(resource)
            .fetchSize(fetchSize)
            .timeout(timeout)
            .statementType(statementType)
            .keyGenerator(keyGenerator)
            .keyProperty(keyProperty)
            .keyColumn(keyColumn)
            .databaseId(databaseId)
            .lang(lang)
            .resultOrdered(resultOrdered)
            .resultSets(resultSets)
            .resultMaps(getStatementResultMaps(resultMap, resultType, id))
            .resultSetType(resultSetType)
            .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
            .useCache(valueOrDefault(useCache, isSelect))
            .cache(currentCache);
    
        ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
        if (statementParameterMap != null) {
          statementBuilder.parameterMap(statementParameterMap);
        }
    
        MappedStatement statement = statementBuilder.build();
    
        // 添加到configuration的mappedStatements集合中缓存起来
        configuration.addMappedStatement(statement);
        return statement;
    }
    
  • Configuration的addMappedStatement方法实现:

    public void addMappedStatement(MappedStatement ms) {
        // id对应mapper接口的方法名
        mappedStatements.put(ms.getId(), ms);
    }
    

动态SQL实现

  • 由以上分析可知,增删查改select|insert|update|delete对应的每个SQL节点对应一个mapping包的SqlSource类对象实例。SqlSource接口的定义如下:在应用代码调用mapper节点的方法时,在内部通过对应的MappedStatement调用其所关联的SqlSource的getBoundSql方法,获取需要执行的SQL语句。

    // 保存在mapper.xml或者使用注解定义的mybatis风格的sql语句
    public interface SqlSource {
      BoundSql getBoundSql(Object parameterObject);
    }
    
  • BoundSql的定义如下:其中sql属性就是实际执行的SQL语句。

    // SQL语句对象类,对于动态参数已经替换为了SQL标准的问号?,并已经记录好了参数的顺序信息
    public class BoundSql {
    	private final String sql; // SQL语句,带问号?的SQL语句
    	private final List<ParameterMapping> parameterMappings; // SQL参数名
    	private final Object parameterObject; // 参数名和参数值映射集合
    	private final Map<String, Object> additionalParameters;
    	private final MetaObject metaParameters;
    	
    	...
    
    }
    
  • 在XMLStatementBuilder的parseStatementNode方法中,通过scripting包的xmltags子包的LanguageDriver的createSqlSource方法来创建对应的SqlSource对象,对应xml的实现类为XMLLanguageDriver。XMLLanguageDriver的createSqlSource方法实现如下:通过scripting包的xmltags子包的XMLScriptBuilder的parseScriptNode方法来解析该增删查改对应的xml节点。

    public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
        XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
        return builder.parseScriptNode();
    }
    
  • XMLScriptBuilder的构造函数实现如下:

    public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
        super(configuration);
        this.context = context;
        this.parameterType = parameterType;
        // 初始化子节点处理器
        initNodeHandlerMap();
     }
    
    
      // 初始化各子节点的处理器,并保存在nodeHandlerMap
    private void initNodeHandlerMap() {
        nodeHandlerMap.put("trim", new TrimHandler());
        nodeHandlerMap.put("where", new WhereHandler());
        nodeHandlerMap.put("set", new SetHandler());
        nodeHandlerMap.put("foreach", new ForEachHandler());
        nodeHandlerMap.put("if", new IfHandler());
        nodeHandlerMap.put("choose", new ChooseHandler());
        nodeHandlerMap.put("when", new IfHandler());
        nodeHandlerMap.put("otherwise", new OtherwiseHandler());
        nodeHandlerMap.put("bind", new BindHandler());
    }
    
  • XMLScriptBuilder的parseScriptNode方法,定义解析增删改查节点,同时判断是否为动态SQL,如果是则使用
    DynamicSqlSource来封装,否则使用RawSqlSource。

    public SqlSource parseScriptNode() {
        // 解析获取一个SqlNode节点结合,通过MixedSqlNode来封装
        // 在parseDynamicTags方法中,会根据节点信息,判断是否为动态SQL,为isDynamic设值
        MixedSqlNode rootSqlNode = parseDynamicTags(context);
        SqlSource sqlSource = null;
        if (isDynamic) {
          // 动态SQL,即包含if,foreach等节点的
          sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
        } else {
          // 普通静态SQL,不包含if,foreach等节点的,但可以存在使用#{}设置的动态参数
          sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
        }
        return sqlSource;
    }
    
  • 动态SQL对应的DynamicSqlSource的定义如下:由parseScriptNode方法可知,rootSqlNode的类型为MixedSqlNode,MixedSqlNode包含了if,foreach等节点的集合,如if节点对应scripting包的xmltags子包的IfSqlNode。

    // 动态SQL,即包含动态参数的SQL
    public class DynamicSqlSource implements SqlSource {
    
      private final Configuration configuration;
      private final SqlNode rootSqlNode;
    
      ...
      
    }
    
    public class MixedSqlNode implements SqlNode {
      private final List<SqlNode> contents;
    
      // 包含sql节点集合,如trim,where,if等
      public MixedSqlNode(List<SqlNode> contents) {
        this.contents = contents;
      }
      
      ...
    
    }
    
    public class IfSqlNode implements SqlNode {
      private final ExpressionEvaluator evaluator;
      private final String test;
      private final SqlNode contents;
      
      ...
    
    }
    
  • 其中是否为动态SQL的判断:对于纯文本,则根据是否存在使用${}来定义的动态SQL语句参数;对于SQL内部存在xml节点,如if, foreach等,则默认属于动态SQL;对于纯文本中存在#{}定义的字符串参数,不使用动态SQL。具体在parseDynamicTags方法中定义。

    protected MixedSqlNode parseDynamicTags(XNode node) {
        List<SqlNode> contents = new ArrayList<SqlNode>();
        NodeList children = node.getNode().getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
          XNode child = node.newXNode(children.item(i));
    
          // 纯文本SQL,则判断是否存在"${"和"}"对,则说明为动态SQL
          if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
            String data = child.getStringBody("");
            TextSqlNode textSqlNode = new TextSqlNode(data);
            
            // 如果是纯文本,即select|insert|update|delete节点内部,不存在if, foreach等子节点时,
            // 存在"${"和"}"对,则说明为动态SQL
            if (textSqlNode.isDynamic()) {
              contents.add(textSqlNode);
              isDynamic = true;
            } else {
              contents.add(new StaticTextSqlNode(data));
            }
    
          // SQL内部包含xml节点,即if, foreach, choose等节点,则是动态SQL
          } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
            String nodeName = child.getNode().getNodeName();
            NodeHandler handler = nodeHandlerMap.get(nodeName);
            if (handler == null) {
              throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
            }
            handler.handleNode(child, contents);
    
            // 设置为true
            isDynamic = true;
          }
        }
        return new MixedSqlNode(contents);
    }
    
  • $ 与 # 的区别为:$是将传入的数据直接显示生成sql语句,#是将传入的值当做字符串的形式。如下:

    select id,name,age from student where id =#{id}

    #:select id,name,age from student where id ='1'
    $:select id,name,age from student where id = 1
    
 类似资料: