Mybatis源码分析四之Mapper映射文件解析过程

一、Mapper.xml解析

前文分析了Configuration的解析过程,本文接着前文继续分析mapper映射文件的解析过程,前文可知映射文件的解析过程是由XMLMapperBuilder来完成的,所以我们可以跟着起parse方法一探究竟。

  public void parse() {
	//判断是否已经加载过
    if (!configuration.isResourceLoaded(resource)) {
    //解析映射文件
      configurationElement(parser.evalNode("/mapper"));
      //添加已经加载的资源
      configuration.addLoadedResource(resource);
      //绑定maper对象和mapper命名空间
      bindMapperForNamespace();
    }
	  //解析未完成的结果集
    parsePendingResultMaps();
    //解析未完成的缓存
    parsePendingCacheRefs();
    //解析未完成的sql语句
    parsePendingStatements();
  }

configurationElement(parser.evalNode("/mapper")):
这个方法就是解析整个mapper.xml文件的入口,这里我们可以去官网查看映射文件包含哪些属性以及其含义是什么,这里就不展开。
在这里插入图片描述也是针对这些属性来解析

  private void configurationElement(XNode context) {
    try {
    //获取命名空间,也就是mapper接口的全限定名
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.isEmpty()) {
        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"));
      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);
    }
  }
  1. 解析其它命名空间的缓存,主要是用来共享其它命名空间的缓存设置,也就是建立一个引用关系
  2. 解析缓存,mybatis默认是开启一级缓存,一级缓存只是针对同一个SqlSession实例,不同的SqlSession实例就需要配置二级缓存,也就是只要在映射文件上加上即可,可以缓存所有的select语句,然后增删改会刷新缓存,缓存的清楚策略有LRU最少使用,FIFO根据对象进入缓存的顺序来清楚,SOFT引用,虚拟机内存不足的时候会被清楚,最后一个WEAK弱引用,虚拟机进行垃圾回收的时候会被清除。可以通过eviction来配置,默认是LRU,flushInterval可以定义刷新间隔(毫秒单位)、size引用数目默认是是1024,readOnly,默认是false,设置为true缓存返回的对象都是一样,且不可修改,可以提升相应性能,false返回的缓存对象的拷贝速度慢,但是更安全。我们也可以实现自定义的缓存只需要实现接口Cache即可。最终会向Configuration的caches数组中添加对应的缓存实例。
  3. parameterMap不做分析(未来可能别移除)
  4. 解析结果集映射,就是把表中的属性值与java对象建立对应关系,我们先简单介绍一下resultMap标签属性和子标签。
    属性:
    id:resultMap的唯一标识
    type:resultMap需要转换的java类型
    extends:继承父resultMap
    autoMapping:自动映射(true|false)
    子标签:
    constructor:构造方法,也就是你的java类型是有参构造的时候,可以在这里配置对应的构造参数,这个标签下有两个子标签idArg标识作为Id的结果,也就是对应id,arg就是普通的参数对应关系
    id:ID值对应
    result:就是建立表中与java对象属性值的对应关系,有五个属性值property表示java中的属性,column表中对应列名,javaType java类的全限定名,jdbcType JDBC类型,typeHandler类型转换器(已经分析过)
    association :关联,处理一对一数据类型,关系表中一般通过某一个属性值来建立对应关系,所以用户需要设置对应的关联关系字段。有两种管理关系select和嵌套结果集
    select关联:设置好属性名、以及数据库中关联的字段名、查询的名称即可,所以不是很难。这里会存在一个N+1问题。
    N+1问题: 简单来说就是你主表查询的时候,会同时去查询关联表中的数据,这单独去查询关联表中的就是+1问题。mybatis采用延迟加载方式解决了此类问题,fetchType=lazy即可,也可以设置全局的lazyLoadingEnabled=true。(需要注意的是当调用对象的equals、clone、hashCode、toString方法的时候就会触发延迟加载,可以通过lazyLoadTriggerMethods参数配置哪些方法需要触发多个方法逗号隔开)
    关联的嵌套结果映射:如果不想因为延迟加载带来的性能问题,也可以采用关联的嵌套结果映射,思路就是通过数据库查询的连接操作,使用一个sql语句把所需要的信息查询出来,然后在结果映射的时候配置好关联的方式即可(参见官网)。
    tip:columnPrefix这个属性的作用是,当嵌套查询有多个一样的数据类型时,可以通过指定不同的别名来区分,比如一篇论文两个作者,那么论文属性中就会有作者A和作者B,两个作者的数据模型是一模一样。那么作者B就可以指定一个别名pf_作者B,那么只需要配置columnPrefix="pf_"即可。
    关联的多结果集(ResultSet):有的数据库可以采用存储过程一次性返回多个结果集,也就是执行多个sql语句,可以配置属性resultSets来解析多个结果集,利用resultSet来指定对应的结果集(参见官网
    集合:集合是处理多个关联类型,比如一个作者有多篇文章,可以利用标签< collection/>表示,里边有个ofType属性,表示的是集合中存放的元素值(参见官网)。
    当我们使用关联时要注意映射时的性能开销。
    鉴别器:< discriminator />可以理解为java中的switch,也就是可以根据不同的值来指定不同的映射,case语句(参见官网)。
    自动映射:自动映射就是在没有设置映射的时候,mybatis会自动把查询返回的列名和java对象中查找相同的列名(忽略大小写)对应,我们知道数据库设计的时候,会采用下划线等方式分隔多个单词,而java命名的时候采用驼峰式命名规则,所以可以配置 mapUnderscoreToCamelCase =true,就会自动的映射。有三种自动映射级别NONE:只能手动映射,PARTIAL:对除在内部定义了嵌套结果映射(也就是连接的属性)以外的属性进行映射,FULL - 自动映射所有属性,默认是PARTIAL。
    源码级的数据结构: 首先把每个子标签转换成ResultMapping数据结构,最后在转换成ResultMap数据结构。最后在把ResultMap保存到resultMaps集合中这是一个map集合id是mapper接口全限定名加上resultMap的id值组合。
  5. 解析sql标签
    sql标签是用来定义可以重用的sql代码片段(官网有很好的样例),解析后的标签保存在Configuration的sqlFragments属性中。
  6. 解析增删改查
  private void buildStatementFromContext(List<XNode> list) {
  //如果配置了数据库提供商,那么就先匹配带有databaseId的语句
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    //继续匹配不带databaseId的语句如果都存在这个将被舍弃
    buildStatementFromContext(list, null);
  }

增删改查语句的解析是通过XMLStatementBuilder进行,调用其parseStatementNode()方法。

public void parseStatementNode() {
		//获取id值,也就是方法名称
    String id = context.getStringAttribute("id");
    //获取数据库提供商id
    String databaseId = context.getStringAttribute("databaseId");
	 //判断当前的databaseId是否正确匹配,如果存在,则返回
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }//获取节点名称select/insert/update/delete
    String nodeName = context.getNode().getNodeName();
    //把当前标签名转换成枚举类型
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    //判断是否是查询语句
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    //如果没有设置flushCache,则除了select语句外都是true
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    //查询语句默认使用缓存
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    //缓存主行结果集,当后续使用相同结果集的时候,就直接从缓存中取,默认是false
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    //解析包含include的标签的语句
    includeParser.applyIncludes(context.getNode());
	//获取参数类型
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
	//获取语言驱动器,默认是XMLLanguageDriver
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);
    //解析selectKey语句,selectKey主要是为了应对那些不支持自动生成主键id的数据库或驱动,他的功能就是通过sql生成对应的主键id,可以在数据插入之前或之后更新主键id值,这样就达到了这样的效果
    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语句
    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");
	//保存MappedStatement对象到Configuration对象的mappedStatements属性中
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

具体的sql语句是以MappedStatement对象保存的,key值就是mapper接口对应的全限定名和方法名称组合,后续要执行sql语句的时候,首先要获取这个对象。

bindMapperForNamespace():
这个方法是把命名空间转换成接口的class对象,然后调用configuration.addMapper(boundType)方法,把接口对象添加到mapperRegistry属性中,该属性是通过Map<Class<?>, MapperProxyFactory<?>>集合对象保存,也就是最终的接口knownMappers.put(type, new MapperProxyFactory<>(type))是被封装成MapperProxyFactory对象保存。

二、总结

Mybatis会使用XMLMapperBuilder对象来解析映射文件,然后把映射文件中的各种标签通过对应的数据结构保存到Configuration对象中。

  1. cache-ref:引用缓存保存到cacheRefMap集合
  2. cache:封装成一个Cache实例,然后保存在Map<String, Cache> caches集合
  3. resultMap:他的数据结构是ResultMap对象,保存在Map<String, ResultMap> resultMaps集合
  4. sql:这个是通过标签的id和标签值直接保存在Map<String, XNode> sqlFragments集合中
  5. select|insert|update|delete:封装成MappedStatement,保存在mapperRegistry属性中

以上就是粗略的对映射文件解析过程进行了分析,到这里整个Configuration对象的解析过程也结束,这对后续的源码分析有很大的帮助。

以上,有任何不对的地方,请指正,敬请谅解。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

菜鸟+1024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值