Mybatis - 一文搞懂 Mybatis 究竟是如何解析SQL语句(上)

一文搞懂 Mybatis 究竟是如何解析SQL语句(上)

前言:

本文紧接着 Mybatis是如何创建出SqlSessionFactory 一文进行详细分析,在上一篇文章中主要描述了 SqlSessionFactory 被创建的执行逻辑,那么在本文中,我们将详细探讨在创建出 SqlSessionFactory 的逻辑中,是如何解析 SQL 语句进行绑定的。那么本文中主要讲解上一篇文章 6 小节中 mapperElement(root.evalNode("mappers")); 这一句代码执行的逻辑,那么如果还没有了解 SqlSessionFactory 是如何被创建出来的,那么就需要打打基础了。

一、解析 mappers 标签

Mybatis 全局配置文件 mybatis-config.xml 中,配置了 <mappers> 标签,主要作用是为了让 mybatis 框架知道在启动的时候应该扫描那个文件,用户自定义的 SQL 放入哪个文件中,以下就是 mybatis-config.xmlmappers 标签内容:

<mappers>
    <mapper resource="UserMapper.xml"/>
</mappers>

Mybatis是如何创建出SqlSessionFactory 可以了解到,Mybatis主要是以靠 mybatis-config.xml 进行解析,那么接下来,我们进入 org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement(org.apache.ibatis.parsing.XNode) 方法中,探讨以下 UserMapper.xml 中是如何被解析为 SQL 并进行参数绑定的

UserMapper.xml 读取
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.onebatis.UserMapper">
    
    <select id="getUserPage" resultType="com.example.onebatis.User">
        select id, user_name, password, address, phone from t_user where phone = #{phone}
    </select>

</mapper>
private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      // 不同的定义方式的扫描,最终都是调用 addMapper()方法(添加到 MapperRegistry)。这个方法和 getMapper() 对应
      // package	包
      if ("package".equals(child.getName())) {
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else {
        // 获取 resource 属性 UserMapper.xml
        String resource = child.getStringAttribute("resource");
        // 获取 url 属性
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        // 根据配置,我们配置的是 resource 属性,那么这里的 resource 一定不为空
        // url 没有配置,url == null
        // mapperClass 也没有配置,mapperClass == null
        if (resource != null && url == null && mapperClass == null) {
        // **********************被关注的逻辑***************************
          // resource	相对路径
          ErrorContext.instance().resource(resource);
          InputStream inputStream = Resources.getResourceAsStream(resource);
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          // 解析 Mapper.xml,总体上做了两件事情 >>
          mapperParser.parse();
          // *********************被关注的逻辑****************************
        }
        // 不走此处逻辑 
        else if (resource == null && url != null && mapperClass == null) {
          // url	绝对路径
          ErrorContext.instance().resource(url);
          InputStream inputStream = Resources.getUrlAsStream(url);
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
          mapperParser.parse();
        } else if (resource == null && url == null && mapperClass != null) {
          // class 	单个接口
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          configuration.addMapper(mapperInterface);
        } else {
          throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
        }
      }
    }
  }
}

根据上面代码中的注释,我们可以很清晰的根据配置文件得到被执行的逻辑,那么我将被执行的逻辑代码片段重新提取。这里对对上面方法进行总结,以上的方法,主要是拿到 mybatis-config.xmlmappers 节点数据,通过不同类型的定义进行不同方式的加载扫描,第一种 package,第二种 resource 被定义,第三种 url 被定义,第四种 mapperClass 被定义,最终无论走的是哪一种逻辑,那么都会定位到我们自己配置的 UserMapper.xml 文件,其余三种方式这里不做详细解释。

// resource 是被加载到的文件名字,因为我们放到了 classpath 下,所以没有任何其他目录,所以当前 resouce = UserMapper.xml
// 这里进行读取文件,想要自己实现,其实可以使用 hutool 工具包中 ClasspathUtil 实现,将文件读成 Stream
InputStream inputStream = Resources.getResourceAsStream(resource);
// 创建一个 mapper xml 的解析器
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 调用解析器的 parse 方法对mapper xml 进行解析, 1.2 小节将讲解此方法
mapperParser.parse();

二、UserMapper.xml 解析

根据上述内容,大致知道了 Mapper xml 的加载流程,在本文中我们不关心它究竟是如何被加载的,我们只关心是如何被解析的 SQL,那么接下来进入 mapperParser.parse(), 代码如下:

public void parse() {
  // 先检查 xml 是否已经被加载过了,如果被加载过了,就不需要进行加载了
  if (!configuration.isResourceLoaded(resource)) {
    // 开始解析 mapper 标签,那么这个 <mapper> 标签就是 UserMapper.xml 的顶层标签
    configurationElement(parser.evalNode("/mapper"));
    // 解析完毕后,将 UserMapper.xml 进行标记已解析
    configuration.addLoadedResource(resource);
    // 把namespace(接口类型)和工厂类绑定起来,放到一个map。
    // 一个namespace 一个 MapperProxyFactory >>
    bindMapperForNamespace();
  }
  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}

那么上述逻辑中,很清晰的就能看出, mapper 标签是 UserMapper.xml 中顶层标签,既然这里标记解析顶层标签,那么子标签的解析一定都在 configurationElement(XNode) 中,下面的代码进入此方法内:

private void configurationElement(XNode context) {
  try {
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    // mapper  -> 父标签
    //    cache  -> 子标签
    //    parameterMap ...  -> 子标签
    //    insert/delete/update/select  -> 子标签
    builderAssistant.setCurrentNamespace(namespace);
    // 添加缓存对象
    cacheRefElement(context.evalNode("cache-ref"));
    // 解析 cache 属性,添加缓存对象
    cacheElement(context.evalNode("cache"));
    // 创建 ParameterMapping 对象
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    // 创建 List<ResultMapping>
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    // 解析可以复用的SQL
    sqlElement(context.evalNodes("/mapper/sql"));
    // 解析增删改查标签,得到 MappedStatement  ***
    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);
  }
}

这里不难看出,每个括号中定义的字符串,都是属于 UserMapper.xml 的标签

2.1 解析 CRUD 标签

1.2 小节最后一段代码中,不仅解析 CRUD 标签,而且还真针对 cache-refcaheparamterMapresultMapsql 标签进行了解析,在本文中都不进行展开,那么我们主要针对 selectinsertupdatedelete 标签的解析进行解读,进入 buildStatementFromContext() 方法,代码如下:

private void buildStatementFromContext(List<XNode> list) {
  // 这里我们并没有配置任何 databaseId 属性,那么这里一定为 null,所以 if 条件不成立
  if (configuration.getDatabaseId() != null) {
    buildStatementFromContext(list, configuration.getDatabaseId());
  }
  // 这里就进入了解析逻辑
  buildStatementFromContext(list, null);
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  for (XNode context : list) {
    // 用来解析增删改查标签的 XMLStatementBuilder
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
      // 解析 Statement,添加 MappedStatement 对象 >>
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteStatement(statementParser);
    }
  }
}

这里要解释以下,为什么解析方法被命名为 buildStatemenet,而不是其他的名字。在 mybatis 中,CRUD 标签被称为 statement, 而 statement 被翻译为中文标识 <声明、清单…>,那么我们作为使用 mybatis的用户,针对 mybatis 可以理解为员工,我们要他给我干活,给他指明它要干的活,那么它自己就需要知道具体要干哪些活,列出清单。这里解释有点牵强。
Mybatis 解析 statement 有专有的 XMLStatementBuilder 解析器进行解析,从上述代码中,直观就能看出 statementParse.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);
  boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
  XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
  includeParser.applyIncludes(context.getNode());
  String parameterType = context.getStringAttribute("parameterType");
  Class<?> parameterTypeClass = resolveClass(parameterType);

  String lang = context.getStringAttribute("lang");
  LanguageDriver langDriver = getLanguageDriver(lang);
  processSelectKeyNodes(id, parameterTypeClass, langDriver);
  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进行了解析,那么这句代码则是众多代码中的核心语句,其他的我们都暂时不关心
  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");
  String parameterMap = context.getStringAttribute("parameterMap");
  String resultType = context.getStringAttribute("resultType");
  Class<?> resultTypeClass = resolveClass(resultType);
  String resultMap = context.getStringAttribute("resultMap");
  String resultSetType = context.getStringAttribute("resultSetType");
  ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
  if (resultSetTypeEnum == null) {
    resultSetTypeEnum = configuration.getDefaultResultSetType();
  }
  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);
}
2.2 创建SQL解析器

进入代码 langDriver.createSqlSource() 中,LanguageDriver 对象是接口,本文中我们主要是使用了 XML 方式,所以毫无疑问进入 XMLLanguageDrivercreteSqlSource(),代码如下:

@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
  // 创建一个 xml 脚本解析器
  XMLScriptBuilder builder = new XMLScriptBuilder(configuration, 
  script, parameterType);
  // 开始解析 node 节点
  return builder.parseScriptNode();
}

// 本方法是 XMLScriptBuilder 类中的
public SqlSource parseScriptNode() {
  // 此句代码主要完成逻辑
  // 1. 如果标签内没有子标签的情况,将占位符 ${} 解析为 ? 
  // 2. 如果标签内,包含了 Element 类型的标签,那么将不同类型的 
  // element 交由不同类型的处理器进行解析,解析完毕后返回,并标识sql是否是动态,
  // 将此类成员变量 isDynamic 进行修改
  MixedSqlNode rootSqlNode = parseDynamicTags(context);
  SqlSource sqlSource;
  if (isDynamic) {
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  } else {
  	// 静态sql,将 #{} 替换为 ?
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  }
  return sqlSource;
}

通过以上的代码 parseDynamicTags(context) 方法,主要作用注释在了代码内,那我我们来一探究竟,到底事如何将 ${} 转换为 的,中途又有其他哪些操作,作用是什么?

2.3 区分静态SQL与动态SQL
// 该方法会被递归调用,请注意
// 当前 XNode 对象表示 curd 标签
protected MixedSqlNode parseDynamicTags(XNode node) {
  List<SqlNode> contents = new ArrayList<>();
  // 拿到所有的子标签,例如 <trim> <where> <set> <foreach> <if> <when> ...
  // 此句代码,如果获取不到字表单的时候,NodeList里面依然有一个值
  NodeList children = node.getNode().getChildNodes();
  for (int i = 0; i < children.getLength(); i++) {
    // 将获取到子节点变为 XNode 对象
    XNode child = node.newXNode(children.item(i));
    // 判断当前节点有没有包含子节点
    if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
      // 拿到 标签内定义的 SQL 语句
      String data = child.getStringBody("");
      // 进入解析 SQL 核心代码
      TextSqlNode textSqlNode = new TextSqlNode(data);
      // 判断 SQL 是不是动态sql语句,如果是,则把 Node 对象装入
      // 集合中,等待下一次的处理
      if (textSqlNode.isDynamic()) {
        contents.add(textSqlNode);
        isDynamic = true;
      } 
	  // 直接将 SQL 置为静态SQL,那么这里就不需要做任何处理
	  else {
        contents.add(new StaticTextSqlNode(data));
      }
    } 
	// 包含了字节点,例如<set> <where> ... 等标签
	else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
      String nodeName = child.getNode().getNodeName();
      NodeHandler handler = nodeHandlerMap.get(nodeName);
      handler.handleNode(child, contents);
      isDynamic = true;
    }
  }
  return new MixedSqlNode(contents);
}

那么,mybatis 是如何判断一条sql语句是否是动态sql语句的,接下来,我们进入 textSqlNode.isDynamic() 方法内,由于文章篇幅问题,我这里就将下面的部分挪到了第二篇文章。

三、总结(上)

上半部分的时序图
mybatis 在解析 SQL 语句的时候,区分为 静态SQL动态SQL,那么评判 静态SQL动态SQL 的标准是什么?

parseDynamicTags(XNode node) 代码中,它首先去读取了select (这里用select标签举例)标签内所有的子节点,然后判断每一个节点内是否包含了真正的 XML 标签,如果没有包含,那么就将其定义为静态sql,如果包含了例如 <where> <set> <if> <foreach> 等标签,那么它一定是动态SQL,既然是动态的SQL,那么就会调用不同的动态SQL标签的处理器,分别处理。在类 XMLScriptBuilder 的构造方法中,将所有可能存在的标签,进行对处理器的初始化。

在理解 Mybatis 源码的时候,一定要注意一个事项,除了 Configuration 对象外,绝大多数对象都是与每一个 SQL 语句相关的,意思就是不同的 SQL 语句,所持有对象的地址是不同的,就例如 XMLScriptBuilder 对象,每次 CRUD 标签都会对应一个不同的对象,而不同的对象内不同标签的处理器实例也不同。

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Mybatis-Plus是一个基于Mybatis的增强工具,它简化了Mybatis的使用方式,提供了更加易用的API和更强大的功能。在使用Mybatis-Plus编写SQL语句时,我们可以进行一些优化来提高性能和效率。 首先,可以通过使用合适的索引来优化SQL语句。索引可以加快查询速度,减少数据库的查询时间。需要注意的是,在创建索引时,要根据实际场景和查询需求选择适当的列进行索引,避免过多或冗余的索引。 其次,可以使用合适的分页查询方式进行优化。在查询大量数据时,可以通过分页查询的方式减少返回的数据量,提高查询效率。Mybatis-Plus提供了PageHelper插件,可以方便地进行分页查询。 另外,可以通过合理的SQL语句编写和设计来优化查询性能。避免在查询语句中使用通配符进行模糊查询,因为模糊查询会增加数据库的查询负担。如果查询条件中的某个字段有固定的值,可以将其写为精确查询条件,而不是使用模糊查询。 此外,还可以对需要查询的字段进行筛选,只选择需要的字段,避免返回多余的数据,减少网络传输和查询时间。 最后,为了避免SQL注入攻击,需要使用参数绑定的方式传递参数,而不是直接拼接参数到SQL语句中。这样可以避免特殊字符对SQL语句的破坏和安全问题。 总之,通过合理使用Mybatis-Plus提供的功能和技巧,我们可以对SQL语句进行优化,提高查询性能和效率。同时,还需要根据具体业务场景和需求进行调整和优化,持续改进和优化SQL语句的编写方式。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值