Mybatis源码解析:sql参数处理,原来可以这么简单

16 篇文章 0 订阅
4 篇文章 0 订阅

在这个章节中我们讨论当sql带有参数时,Mybatis是如何处理的。使用的还是User类。

Mybatis源码解析:sql参数处理,原来可以这么简单

 

//省略get set方法
public class User {
    private int id;
    private String name;
    private String phone;
}

例1 带有全局变量的sql

//UserMapper中的dao接口
List<User> getByglobal();
  <select id="getByglobal" resultType="com.entity.User">
      select * from user where id = ${globalId}
  </select>
<!--mybatis.xml中的部分配置-->
  <properties>
    <property name="globalId" value="1"/>
  </properties>

注意我是用的符号为$。在这个例子中globalId是在mybatis.xml文件中的property配置的。接口不传参数。

在学习第二个章节时,我们知道每一个查询语句都会被包装成一个MappedStatement,这个对象用来存放我们的sql语句,返回类型,id等等。让我们回到之前的代码。

  //XMLMapperBuilder.configurationElement
  private void configurationElement(XNode context) {
    try {
      //mapper的缓存信息,命名空间等会被临时保存到MapperBuilderAssistant中,最后把这些公用的信息在存到MappedStatement中
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      //该节点已经被废弃
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      //在解析增删改查节点时,每个节点都会生成一个mapperStatement对象并保存到配置文件类中.
      //mapperStatement保存这这个节点的全部信息,如id,fetchSize,timeout
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

这部分代码应该不陌生,第二章分析了buildStatementFromContext()方法,现在我们从context.evalNodes("select|insert|update|delete")开始。该方法开始解析增删改查的节点

  //XNode.evalNodes
  public List<XNode> evalNodes(String expression) {
    return xpathParser.evalNodes(node, expression);
  }
  public List<XNode> evalNodes(Object root, String expression) {
    List<XNode> xnodes = new ArrayList<>();
    NodeList nodes = (NodeList) evaluate(expression, root, XPathConstants.NODESET);
    for (int i = 0; i < nodes.getLength(); i++) {
      //这里创建的新的节点并添加到结合中,解析也是在创建节点的时候开始的。
      xnodes.add(new XNode(this, nodes.item(i), variables));
    }
    return xnodes;
  }

XNode构造方法,在创建新节点的时候,会在构造器中先进性解析,也就是调用parseBody方法.

  public XNode(XPathParser xpathParser, Node node, Properties variables) {
    this.xpathParser = xpathParser;
    this.node = node;
    this.name = node.getNodeName();
    this.variables = variables;
    //获取节点的属性
    //例如这个select就有id和resultType两个属性
    this.attributes = parseAttributes(node);
    //解析节点里面的内容,也就是sql了
    this.body = parseBody(node);
  }

variables传入配置中的全局变量

  //XNode.parseBody
  private String parseBody(Node node) {
    //获取当前节点的信息
    //例子中这里返回空
    String data = getBodyData(node);
    if (data == null) {
      //获取孩子节点的信息
      NodeList children = node.getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
        Node child = children.item(i);
        //获取当前节点的信息
        data = getBodyData(child);
        if (data != null) {
          break;
        }
      }
    }
    return data;
  }

为什么select节点getBodyData会返回空呢,从它的方法体中可以看出,首先它会判断节点的类型,select这个节点是ELEMENT_NODE类型,不属于它要求的文本类型或者部分节点类型。那么就直接返回空了。而当select的孩子节点,也就是sql语句select * from user where id = ${globalId}这个节点调用getBodyData方法时,sql语句是文本类型的,满足条件,才会使用解析器开始解析。

  //XNode.getBodyData
  private String getBodyData(Node child) {
    //判断节点的类型
    if (child.getNodeType() == Node.CDATA_SECTION_NODE
        || child.getNodeType() == Node.TEXT_NODE) {
      String data = ((CharacterData) child).getData();
      data = PropertyParser.parse(data, variables);
      return data;
    }
    return null;
  }
  //PropertyParser.parse
  public static String parse(String string, Properties variables) {
    //先创建一个处理器
    VariableTokenHandler handler = new VariableTokenHandler(variables);
    //创建解析器
    GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
    //进行解析
    return parser.parse(string);
  }

这里出现了很多陌生的类。首先是GenericTokenParser通用类型的解析器,他能根据传入的参数做出相应。如果参数满足条件,就会调用handler处理器来处理参数。每个handler都要实现handleToken方法,该方法就是用来处理参数的。

例如这里传入的是以${作为开头,}作为结尾。如果传入的字符串包含一个或者多个这样的格式,就会调用VariableTokenHandler.handleToken,该方法会试图从全局中找到该变量,并修改成具体的值。

VariableTokenHandler.handleToken 传入String变量globalId,将其替换成1并返回。

    public String handleToken(String content) {
     //variables里面存放全局的变量,为空直接return
      if (variables != null) {
        String key = content;
        //是否存在默认值,默认是false
        if (enableDefaultValue) {
          final int separatorIndex = content.indexOf(defaultValueSeparator);
          String defaultValue = null;
          if (separatorIndex >= 0) {
            key = content.substring(0, separatorIndex);
            defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
          }
          if (defaultValue != null) {
            return variables.getProperty(key, defaultValue);
          }
        }
        //variables是用来存放全局变量的容器。
        //这里会从全局变量中找到我们定义的globalId,然后将对应的值返回,这样我们的sql就拼接完成了
        if (variables.containsKey(key)) {
          return variables.getProperty(key);
        }
      }
      return "${" + content + "}";
    }
  }
  

解析器代码,根据传入的标记开始解析,这里传入开始标记${和结束标记$}。在这之后还会用来解析#{}。代码比较长,最好打个断点进去看。

//GenericTokenParser.parse
public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    //查找开始标记,如果不存在返回-1 ,存在返回偏移量
    int start = text.indexOf(openToken);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    //这个变量用来存放中间的字符,如${id}中的id
    StringBuilder expression = null;
    //如果存在开始标志
    while (start > -1) {
      //这里将从offset开始,一直到start的字符先放入builder中
      //例如select * from user where id = 
      if (start > 0 && src[start - 1] == '\\') {
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        //更新偏移量
        offset = start + openToken.length();
        //找到与开始标志对应的结束标志
        int end = text.indexOf(closeToken, offset);
        //取到中间字符globalId
        while (end > -1) {
          if (end > offset && src[end - 1] == '\\') {
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {
            expression.append(src, offset, end - offset);
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          //这里根据不同的处理器会有不同的操作,刚才传入的是VariableTokenHandler
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }

到这里全局变量就解析完成了,那么如果在全局变量中没有找到对应的值该怎么办呢?例如我这里使用的sql是select * from user where id = ${id},而不是${globalId},那么根据VariableTokenHandler处理器,它会原封不动的进行返回,等待后文的解析。

顺便一提,这一部分的解析实在解析我们的配置文件的时候就发生了,方法入口为context.evalNodes("select|insert|update|delete"),在解析配置的时候,其他节点也大量使用了context.evalNodes()方法去解,所以只要当配置mybatis.xml文件中的properties节点解析完成之后,里面的变量就是能全局使用了,这也是为什么properties节点要放在第一个解析。

又由于这个通用解析器只解析${XXX}格式的变量,所以全局的变量不能写成#{xxx}.

入参${}的解析

List<User> get(Integer id);
  <select id="get" resultType="com.entity.User">
      select * from user where id = ${id}
  </select>

这个例子,我们没有在全局变量中定义id,而是在方法中传入这个值。根据上文中的VariableTokenHandler.handleToken方法就会返回${id},表示这个参数全局变量中没有,是待解析的参数。

这是解析buildStatementFromContext(context.evalNodes("select|insert|update|delete"));的后续代码,用来解析标签,并创建mappedStaement,在第二章中也分析过,这里直接copy过来.

  //XMLStatementBuilder.parseStatementNode
  public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    //是否刷新缓存 默认值:增删改刷新 查询不刷新
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    //是否使用二级缓存 默认值:查询使用 增删改不使用
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    //是否需要处理嵌套查询结果 group by

    // 三组数据 分成一个嵌套的查询结果
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    //替换Includes标签为对应的sql标签里面的值
    includeParser.applyIncludes(context.getNode());

    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    //解析配置的自定义脚本语言驱动 mybatis plus
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    //解析selectKey
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    //设置主键自增规则
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }
    //解析Sql  根据sql文本来判断是否需要动态解析 如果没有动态sql语句且 只有#{}的时候 直接静态解析使用?占位 当有 ${} 不解析
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    //暗示驱动程序每次批量返回的结果行数
    Integer fetchSize = context.getIntAttribute("fetchSize");
    //超时时间
    Integer timeout = context.getIntAttribute("timeout");
    //引用外部 parameterMap,已废弃
    String parameterMap = context.getStringAttribute("parameterMap");
    //结果类型
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    //引用外部的 resultMap
    String resultMap = context.getStringAttribute("resultMap");
    //结果集类型,FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE 中的一种
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    //(仅对 insert 有用) 标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }
  

找到解析sql的部分具体来分析,一层一层往下。

SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

RawLanguageDriver.createSqlSource 该类是XMLLanguageDriver的子类

  @Override
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    SqlSource source = super.createSqlSource(configuration, script, parameterType);
    checkIsNotDynamic(source);
    return source;
  }

XMLLanguageDriver.createSqlSource

  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
  }

XMLScriptBuilder.parseScriptNode

  public SqlSource parseScriptNode() {
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    //判断节点是否是动态的,包含是否包含if、where 、choose、trim、foreach、bind、sql标签,这个例子中我们进入else
    if (isDynamic) {
      //不解析
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      //用占位符方式来解析
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

这里进行判断isDynamic的值,这个方法我们只需要关注textSqlNode.isDynamic()就行了。代码与之前解析node有些类似。

  protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      //注意!!这里又new了一个XNode,也就是说,这个节点中的sql语句又被解析了一次,解析方式和上文从同全局获取变量一样。
      //与上文不同的是,这里传入的是子节点,也就是sql文本语句,而上文解析的是整个select元素
      //这个child是临时变量,节点解析的结果不做保存
      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()) {
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
          contents.add(new StaticTextSqlNode(data));
        }
      } 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);
        isDynamic = true;
      }
    }
    return new MixedSqlNode(contents);
  }

TextSqlNode.isDynamic

  public boolean isDynamic() {
    DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
    //这里创建一个解析器进行解析sql语句,这里解析的是仍然是${}
    GenericTokenParser parser = createParser(checker);
    parser.parse(text);
    return checker.isDynamic();
  }
  private GenericTokenParser createParser(TokenHandler handler) {
    return new GenericTokenParser("${", "}", handler);
  }

熟悉的代码,还是同样的解析器,用来处理${,和},不过这次的hander不同,为DynamicCheckerTokenParser

  //DynamicCheckerTokenParser.handleToken
  public String handleToken(String content) {
    this.isDynamic = true;
    return null;
  }
}

这次的处理方式是将直接返回空,也就是说,sql会变成 select * from user where id = null。但是返回的结果并没有被保存,parser.parse(text)并没有参数来接受它的返回值,所以这里只是用来更新isDynamic参数。

回到XMLScriptBuilder.parseScriptNode方法,这里根据isDynamic的布尔值,会有两种SqlSource.DynamicSqlSource和RawSqlSource。到这里配置文件就解析完成了,后续sql中的参数都是从方法中获取的,所以只能在执行的时候动态进行替换。

来到query查询方法,方法在第三章执行sql的时候简单说过。ms.getBoundSql会获取绑定的封装sql.

//CachingExecutor.query
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

MappedStatement.getBoundSql

  public BoundSql getBoundSql(Object parameterObject) {
    //获取绑定的sql
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    //获取sql中对应的参数
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings == null || parameterMappings.isEmpty()) {
      boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
    }

    // check for nested result maps in parameter mappings (issue #30)
    for (ParameterMapping pm : boundSql.getParameterMappings()) {
      String rmId = pm.getResultMapId();
      if (rmId != null) {
        ResultMap rm = configuration.getResultMap(rmId);
        if (rm != null) {
          hasNestedResultMaps |= rm.hasNestedResultMaps();
        }
      }
    }

    return boundSql;
  }
//DynamicSqlSource.getBoundSql。
  public BoundSql getBoundSql(Object parameterObject) {
    //parameterObject中有我们方法传入的参数
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    //这里解析${}
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }

为什么是DynamicSqlSource而不是RawSqlSource,这个前文分析过,在替换完全局变量后,语句中如果还包含${},使用的就是DynamicSqlSource。

  //MixedSqlNode.apply
  public boolean apply(DynamicContext context) {
    contents.forEach(node -> node.apply(context));
    return true;
  }

TextSqlNode.apply

  @Override
  public boolean apply(DynamicContext context) {
    contents.forEach(node -> node.apply(context));
    return true;
  }

这里再次创建了${}的解析器,这次的handler是BindingTokenParser

  public boolean apply(DynamicContext context) {
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    context.appendSql(parser.parse(text));
    return true;
  }

  private GenericTokenParser createParser(TokenHandler handler) {
    return new GenericTokenParser("${", "}", handler);
  }

BindingTokenParser.handleToken,如果sql中存在${},就会将其替换成具体的参数,语句就变成 select * from user where id = 1,就能直接执行了

  public String handleToken(String content) {
    Object parameter = context.getBindings().get("_parameter");
    if (parameter == null) {
      context.getBindings().put("value", null);
    } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
      context.getBindings().put("value", parameter);
    }
    Object value = OgnlCache.getValue(content, context.getBindings());
    String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
    checkInjection(srtValue);
    return srtValue;
  }

入参#{}的解析

那么如果是#{}该怎么处理呢?

  <select id="get" resultType="com.entity.User">
      select * from user where id = #{id}
  </select>
  List<User> get(Integer id);

由上文得知,由于没有${},那么SqlSource就会变成RawSqlSource。在创建RawSqlSource的时候,在构造方法中就会对#{}解析。

RawSqlSource的构造方法。

  public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
    this(configuration, getSql(configuration, rootSqlNode), parameterType);
  }

  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
  }

SqlSourceBuilder.parse

  public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

这里用的hander是ParameterMappingTokenHandler,它的作用是将#{XXX}替换成 

ParameterMappingTokenHandler.handleToken

    public String handleToken(String content) {
      parameterMappings.add(buildParameterMapping(content));
      return "?";
    }

这时sql就变成了select * from user where id = ?,到这里还只是解析配置文件。在具体执行方法时也要调用getBoundSql方法将参数进行赋值

  //RawSqlSource.getBoundSql
  public BoundSql getBoundSql(Object parameterObject) {
    return sqlSource.getBoundSql(parameterObject);
  }

StaticSqlSource.getBoundSql,最后调用BoundSql的构造方法,将sql语句,入参等传入

  public BoundSql getBoundSql(Object parameterObject) {
    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
  }

之后就要创建数据库连接,进行查询了。回到这个方法SimpleExecutor.prepareStatement。回顾一下,这是创建StatementHandler后做的一些连接数据库的准备操作。

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
  Statement stmt;
  //获取jdbc数据库连接
  Connection connection = getConnection(statementLog);
  //一些准备工作,初始化Statement连接
  stmt = handler.prepare(connection, transaction.getTimeout());
  //使用ParameterHandler处理入参
  handler.parameterize(stmt);
  return stmt;
}

我们先进入这个方法PreparedStatementHandler.parameterize。

为什么是PreparedStatementHandler之前也说过,因为语句的默认类型是PREPARED, 还有其他的类型如果是CALLABLE,对应CallableStatementHandler,STATEMENT对应SimpleStatementHandler。可以用参数statementType进行设置。

@Override
public void parameterize(Statement statement) throws SQLException {
  parameterHandler.setParameters((PreparedStatement) statement);
}

DefaultParameterHandler.setParameters.

  @Override
  public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    //boundSql用来解析我们的sql语句,parameterMappings是我们传入的参数
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        //这里第一个参数就是id
        ParameterMapping parameterMapping = parameterMappings.get(i);
        //mode属性允许能指定IN,OUT或INOUT参数。如果参数的 mode 为 OUT 或 INOUT,将会修改参数对象的属性值,以便作为输出参数返回。
        //#{id}默认mode为OUT
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          //这里是boundsql中的额外参数,可以使用拦截器添加,例子放在下文
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
            //如果类型处理器中有这个类型,那么直接赋值就行了,例如这里是Integer类型,类型处理器是有的
            //那么直接赋值
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else {
          //如果不是的会转化为元数据进行处理,metaObject元数据可以理解为用来反射的工具类,可以处理参数的get,set
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          //获取类型处理器
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          //获取数据库类型
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            //使用不同的类型处理器向jdbc中的PreparedStatement设置参数
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

value如果是空的那么就直接设置为jdbc的空类型,不为空调用具体的类型处理器。

BaseTypeHandler.setParameter。该类是所有typeHandler的父类.如果不为空调用setNonNullParameter,该方法时抽象的,由具体的子类实现。这里使用的是一个相当于路由的的子类UnknownTypeHandler,这个子类可以根据传入的类型,再去找到具体的类型处理器,例如IntegerTypeHander.

  @Override
  public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
    if (parameter == null) {
      if (jdbcType == null) {
        throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
      }
      try {
        ps.setNull(i, jdbcType.TYPE_CODE);
      } catch (SQLException e) {
        throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "
              + "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "
              + "Cause: " + e, e);
      }
    } else {
      try {
        setNonNullParameter(ps, i, parameter, jdbcType);
      } catch (Exception e) {
        throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . "
              + "Try setting a different JdbcType for this parameter or a different configuration property. "
              + "Cause: " + e, e);
      }
    }
  }

UnknownTypeHandler.setNonNullParameter

  public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
      throws SQLException {
    TypeHandler handler = resolveTypeHandler(parameter, jdbcType);
    handler.setParameter(ps, i, parameter, jdbcType);
  }

UnknownTypeHandler.resolveTypeHandler这个方法根据传入的参数类型,找到具体的TypeHandler

  private TypeHandler<?> resolveTypeHandler(Object parameter, JdbcType jdbcType) {
    TypeHandler<?> handler;
    if (parameter == null) {
      handler = OBJECT_TYPE_HANDLER;
    } else {
      handler = typeHandlerRegistry.getTypeHandler(parameter.getClass(), jdbcType);
      // check if handler is null (issue #270)
      if (handler == null || handler instanceof UnknownTypeHandler) {
        handler = OBJECT_TYPE_HANDLER;
      }
    }
    return handler;
  }

例如如果这个参数是id,Integer类型,那么就会找到IntegerTypeHandler

  //IntegerTypeHandler
  public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setInt(i, parameter);
  }

最后还是使用jdbc的PreparedStatement处理参数。

附:自定义的拦截器用来加入参数。

@Intercepts({
  @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,Integer.class})
})
public class MyInterceptor implements Interceptor {
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    StatementHandler bs = (StatementHandler) invocation.getTarget();
    BoundSql boundSql = bs.getBoundSql();
    boundSql.setAdditionalParameter("id","1");
    return invocation.proceed();
  }
}

例4 ${}和#{}都存在的情况

如果是都存在的情况呢?

  <select id="findUserByIdAndName" resultType="com.entity.User">
        select * from user where id = ${id} AND name = #{name}
    </select>
  List<User> findUserByIdAndName(@Param("id") Integer id, @Param("name") String name);

结合上文的分析,由于存在${},所以选择的DynamicSqlSource。

DynamicSqlSource.getBoundSql。这个方法上文分析到了rootSqlNode.apply(context);会将${}替换成具体参数。我们接着分析。

  public BoundSql getBoundSql(Object parameterObject) {
    //parameterObject中有我们方法传入的参数
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    //解析${}并替换成具体的值
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    //这里又进行了一次解析
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }

解析#{},并将其替换成?

  //RawSqlSource.parse
  public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

这样我们的语句就变成了select * from user where id = 1 AND name = ?,然后调用sqlSource.getBoundSql

  //StaticSqlSource.getBoundSql
  public BoundSql getBoundSql(Object parameterObject) {
    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
  }

最后的处理方式与例3相同,使用jdbc自带的PreparedStatement进行参数处理。

小结

当我们在解析mapper.xml文件时,就会将sql进行第一遍的解析,将其中的全局变量替换成具体的值。

接着进行第二遍的解析,选择不同的SqlSource。这一边的解析不改变语句中的sql内容。

如果语句中包含${},就选择DynamicSqlSource,等待具体执行sql的时候再做处理.如果仅包含#{}类型的,就选择RawSqlSource。RawSqlSource在创建的时候就会有进行一轮的解析,将语句中的#{XXX}替换为 ?(问号)

之后在执行具体的语句才动态的替换,如果之前选择的是DynamicSqlSource,那么进行两次的解析,第一次将${}替换成具体值,第二次解析#{},使用jdbc的PreparedStatement处理。如果选择的是RawSqlSource,那么这条语句就只有#{},直接用PreparedStatement处理。

可以发现,无论什么类型的sql都会被解析了4次。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值