你的位置:首页 > 信息动态 > 新闻中心
信息动态
联系我们

Mybatis级联映射与懒加载

2021/12/12 19:11:44

级联映射与懒加载

  • 概述
  • 一对多关联映射
  • 一对一关联映射
  • Discriminator详解
  • 级联映射实现原理(重要!!!)
    • ResultMap概述
    • ResultMap解析过程
    • 级联映射实现原理
  • 懒加载机制
  • 懒加载实现原理
    • 总结

概述

所谓的懒加载,就是当在一个实体对象中 关联 其他实体对象时,如果不需要获取被关联的实体对象,则不需要为 被关联的实体 执行 额外的查询操作,仅当 调用 当前实体的Getter方法 获取 被关联实体对象时,才会执行一次额外的查询操作
通过这种方式在一定程度上能够减轻数据库的压力

一对多关联映射

MyBatis的Mapper配置中提供了一个<collection>标签,用于建立实体间一对多的关系

<collection>标签需要嵌套在<resultMap>标签中使用,可以使用<collection>标签为实体A的属性b 关联一个 外部的查询Mapper,使用ofType属性 指定 属性b中 存放的类型(实体B),使用select属性 指定 通过执行相关sql 来为实体A的属性b 填充(动词)值

然后只需要在定义Mapper SQL配置时,通过resultMap属性指定结果集映射即可

除了可以通过<collection>标签 关联一个 外部定义的Mapper 来完成 一对多关联查询外,MyBatis还支持通过JOIN子句实现一对多查询

这种情况下,<collection>标签相当于一个嵌套的ResultMap,通过ofType属性指定 实体A的属性b中存放的类型为实体B,然后通过<result>标签 配置 实体B中的每个属性配置 与 数据库字段之间的映射

一对一关联映射

MyBatis一对一关联映射的配置方式与一对多映射类似,不同的是在定义ResultMap时需要使用<association>标签

在配置ResultMap结果集映射时,通过<association>标签为 实体A的属性b 关联一个 外部SQL Mapper配置,当MyBatis进行结果集映射时,会以b表的字段内容 作为 参数执行一次额外的查询操作,然后使用查询结果 为 实体A的属性b填充值

MyBatis同样支持通过JOIN查询实现一对一级联查询
这种情况下,<association>标签相当于一个嵌套的ResultMap。使用JOIN语句同时查询表a和表b,只需要使用<association>标签 将表b相关的字段 映射到 实体B对应的属性即可

Discriminator详解

该单词的意思是“鉴别器”
从单词含义上并不能看出Discriminator的作用
实际上,MyBatis中的Discriminator类似于Java中的switch语法,能够根据数据库记录中某个字段的值映射到不同的ResultMap

使用<discriminator>标签对表A的字段a进行映射
当字段a值为1时,为实体A的属性a关联一个外部的查询Mapper
当字段a值为2时,则不做映射处理

级联映射实现原理(重要!!!)

ResultMap概述

MyBatis的Mapper配置中提供了一个<resultMap>标签,用于建立数据库字段与Java实体属性之间的映射关系
每个ResultMap需要有一个全局唯一的Id,由<resultMap>标签的id属性指定
除此之外,ResultMap还需要通过type属性 指定 与哪一个Java实体进行映射

<resultMap>标签中,需要使用<id><result>标签 配置 具体的某个表字段 与 Java实体属性之间的映射关系
数据库主键通常使用<id>标签建立映射关系,普通数据库字段则使用<reuslt>标签

除了属性映射外,ResultMap还支持使用构造器映射,构造器映射需要使用<constructor>标签
使用构造器映射的前提是 建立映射的Java实体需要提供对应的构造方法<idArg>标签 用于配置 数据库主键的映射,<arg>标签 用于配置 普通数据库字段的映射

总结:
<resultMap>标签中可以使用下面几种子标签

  • <constructor>:该标签用于建立构造器映射。该标签有两个子标签,<idArg>标签用于配置主键映射,标记出主键,可以提高整体性能;<arg>标签用于配置普通字段的映射
  • <id>:用于配置数据库主键映射,标记出数据库主键,有助于提高整体性能
  • <result>:用于配置数据库字段与Java实体属性之间的映射关系
  • <association>:用于配置一对一关联映射,可以关联一个外部的查询Mapper或者配置一个嵌套的ResultMap
  • <collection>:用于配置一对多关联映射,可以关联一个外部的查询Mapper或者配置一个嵌套的ResultMap
  • <discriminator>:用于配置根据字段值使用不同的ResultMap。该标签有一个子标签,<case>标签用于枚举字段值对应的ResultMap,类似于Java中的switch语法

ResultMap解析过程

MyBatis在启动时,所有配置信息都会被转换为Java对象,通过<resultMap>标签 配置的 结果集映射信息也不例外
MyBatis通过ResultMap类描述<resultMap>标签的配置信息,ResultMap类的所有属性如下:

这些属性的含义如下

  • Id:通过<resultMap>标签的id属性 和 Mapper命名空间组成的全局唯一的Id
  • Type:通过<resultMap>标签的type属性 指定 与数据库表建立映射的Java实体
  • resultMappings:通过<result>标签配置的 所有数据库字段 与 Java实体属性之间的映射信息
  • idResultMappings:通过<id>标签配置的 数据库主键 与 Java实体属性的映射信息。需要注意的是,<id>标签与<result>标签没有本质的区别
  • constructorResultMappings:通过<constructor>标签配置的 构造器映射信息
  • propertyResultMappings:通过<result>标签配置的 数据库字段 与 Java实体属性的映射信
  • mappedColumns:该属性 存放 所有映射的数据库字段。当使用columnPrefix属性配置了前缀时,MyBatis会对mappedColumns属性进行遍历,为所有数据库字段 追加 columnPrefix属性配置的前缀
  • mappedProperties:该属性 存放 所有映射的Java实体属性信息
  • discriminator:该属性为在<resultMap>标签中通过<discriminator>标签配置的鉴别器信息
  • hasNestedResultMaps:该属性 用于标识 是否有 嵌套的ResultMap,当使用<association><collection>标签以JOIN查询方式配置一对一或一对多级联映射时,<association><collection>标签 相当于一个 嵌套的ResultMap,因此hasNestedResultMaps属性值为true
  • hasNestedQueries:该属性 用于标识 是否有 嵌套的查询,当使用<association><collection>标签关联一个外部的查询Mapper建立一对一或一对多级联映射时,hasNestedQueries属性值为true
  • autoMapping:autoMapping属性为true,表示开启自动映射,即使 未使用<result><id>标签配置 映射字段,MyBatis也会自动对这些字段进行映射

解析过程如下:
MyBatis中的Mapper配置信息解析都是通过XMLMapperBuilder类完成的,该类提供了一个parse()方法,用于解析Mapper中的所有配置信息

在XMLMapperBuilder的parse()方法中,调用XMLMapperBuilder类的configurationElement()进行处理

在XMLMapperBuilder类的configurationElement()方法中,调用resultMapElements()方法对所有<resultMap>标签进行解析

resultMapElements()方法最终会调用重载的resultMapElement()方法对每个<resultMap>标签进行解析

在XMLMapperBuilder类的resultMapElement()方法中,首先获取<resultMap>标签的所有属性信息,然后对<id><constructor><discriminator>子标签进行解析,接着 创建一个 ResultMapResolver对象,调用ResultMapResolver对象的resolve()方法 返回一个 ResultMap对象

ResultMapResolver对象的resolve()方法的逻辑非常简单,调用MapperBuilderAssistant对象的addResultMap()方法 创建ResultMap对象,并把ResultMap对象 添加到 Configuration对象中

在MapperBuilderAssistant类的addResultMap()方法中,首先判断该ResultMap 是否继承了 其他ResultMap
如果是,则获取父ResultMap对象,然后 去除父ResultMap中 的 构造器映射信息,将 父ResultMap中 配置的映射信息 添加到 当前ResultMap对象,最后 通过建造者模式 创建ResultMap对象
在ResultMap.Builder类中创建了一个ResultMap对象,然后为ResultMap对象的所有属性赋值

级联映射实现原理

StatementHandler组件 与 数据库 完成交互后,会使用 ResultSetHandler组件 对 结果集 进行处理

在PreparedStatementHandler类的query()方法中,调用PreparedStatement对象的execute()方法完成与数据库交互之后,会调用ResultSetHandler对象的handleResultSets()方法对结果集进行处理

ResultSetHandler接口只有一个默认的实现,即DefaultResultSetHandler类

在DefaultResultSetHandler类的handleResultSets()方法中,为了简化对JDBC中ResultSet对象的操作,将ResultSet对象 包装成 ResultSetWrapper对象,然后 获取MappedStatement对象 对应的 ResultMap对象,接着 调用 重载的handleResultSet()方法进行处理

在handleResultSet()方法中做了一些逻辑判断,最终都会调用DefaultResultSetHandler类的handleRowValues()方法进行处理

在DefaultResultSetHandler类的handleRowValues()方法中 判断 ResultMap中是否有嵌套的ResultMap,当使用<association><collection>标签通过JOIN查询方式进行级联映射时,hasNestedResultMaps()方法的返回值为true

如果有嵌套的ResultMap,则调用handleRowValuesForNestedResultMap()方法进行处理,否则调用handleRowValuesForSimpleResultMap()方法

有嵌套ResultMap时的处理逻辑如下:
handleRowValuesForNestedResultMap()方法 对 结果集对象 进行遍历,处理每一行数据
首先调用resolveDiscriminatedResultMap()方法 处理 <resultMap>标签中 通过<discriminator>标签配置的 鉴别器信息,根据 字段值 获取 对应的ResultMap对象,然后调用DefaultResultSetHandler类的getRowValue()方法将结果集中的一行数据转换为Java实体对象

在getRowValue()方法中,主要做了以下几件事情:

  1. 调用createResultObject()方法 处理 通过<constructor>标签 配置的 构造器映射,根据 配置信息 找到 对应的构造方法,然后通过MyBatis中的ObjectFactory创建ResultMap关联的实体对象

  2. 调用applyAutomaticMappings()方法处理自动映射,对 未通过<result>标签配置映射的数据库字段 进行 与Java实体属性的映射处理

    在applyAutomaticMappings()方法中,首先获取 未指定映射 的 所有数据库字段 和 对应的Java属性,然后获取对应的字段值,通过反射机制为Java实体对应的属性值赋值

  3. 调用applyPropertyMappings()方法 处理 <result>标签配置的 映射信息。该方法处理逻辑相对简单,对所有<result>标签配置的映射信息进行遍历,然后找到数据库字段对应的值,为Java实体属性赋值

  4. 调用DefaultResultSetHandler类的applyNestedResultMappings()方法处理嵌套的结果集映射

    在applyNestedResultMappings()方法中,首先获取 嵌套ResultMap对象,然后根据 嵌套ResultMap的Id 从缓存中 获取嵌套ResultMap 对应的 Java实体对象,如果能获取到,则调用linkObjects()方法 将 嵌套Java实体 与 外部Java实体进行关联。如果缓存中没有,则调用getRowValue()方法 创建 嵌套ResultMap对应的Java实体对象 并进行 属性映射,然后调用linkObjects()方法与外部的Java实体对象进行关联

懒加载机制

在一些情况下,需要按需加载,即当查询用户信息时,如果不需要获取用户订单信息,则不需要执时订单查询对应的Mapper,仅当调用Getter方法获取订单数据时,才执行一次额外的查询操作。这种方式能够在一定程度上能够减少数据库IO次数,提升系统性能

MyBatis中提供了懒加载机制,能够帮助我们实现这种需求
MyBatis主配置文件中提供了lazyLoadingEnabledaggressiveLazyLoading参数用来控制是否开启懒加载机制

  • lazyLoadingEnabled参数值为true时表示开启懒加载,否则表示不开启懒加
  • aggressiveLazyLoading参数用于控制ResultMap默认的加载行为,参数值为false表示ResultMap默认的加载行为为懒加载,否则为积极加载

除此之外,<collection><association>标签还提供了一个fetchType属性,用于控制 级联查询 的 加载行为,fetchType属性值为lazy时 表示 该级联查询 采用懒加载方式,当fetchType属性值为eager时表示该级联查询采用积极加载方式

懒加载实现原理

在DefaultResultSetHandler类的handleRowValues()方法中处理结果集时,对嵌套的ResultMap和非嵌套ResultMap做了不同处理

ResultMap对象的hasNestedResultMaps属性值为false,hasNestedQueries属性值为true

MyBatis框架在开启懒加载机制后,handleRowValues()方法 会调用 handleRowValuesForSimpleResultMap()方法 处理 ResultMap映射

在handleRowValuesForSimpleResultMap()方法中,首先调用skipRows()方法 跳过 RowBounds对象指定偏移的行,然后遍历结果集中所有的行,对<discriminator>标签配置的鉴别器进行处理,获取 实际映射的ResultMap对象,接着调用getRowValue()方法处理一行记录,将 数据库行记录 转换为 Java实体对象

在getRowValue()方法中主要做了下面几件事情:

  1. 创建ResultLoaderMap对象,该对象用于存放 懒加载的属性 及 对应的ResultLoader对象,MyBatis中的ResultLoader用于执行一个查询Mapper,然后将执行结果赋值给某个实体对象的属性
  2. 调用createResultObject()方法创建ResultMap对应的Java实体对象,createResultObject()方法中,首先调用重载的createResultObject()方法 使用ObjectFactory对象 创建 Java实体对象,然后判断ResultMap中是否有嵌套的查询,如果有嵌套的查询 并且 开启了 懒加载机制,则通过MyBatis中的ProxyFactory创建实体对象的代理对象。ProxyFactory接口有两种不同的实现,分别为CglibProxyFactory和JavassistProxyFactory。也就是说,MyBatis同时支持使用Cglib和Javassist创建代理对象,具体使用哪种策略创建代理对象,可以在MyBatis主配置文件中通过proxyFactory属性指定
  3. 调用applyAutomaticMappings()方法处理自动映射
  4. 调用applyPropertyMappings()方法处理<result>标签配置的映射字段,该方法中除了为Java实体属性设置值外,还将指定了懒加载的属性添加到ResultLoaderMap对象中

总结

MyBatis中的懒加载实际上是通过动态代理来实现的
当通过MyBatis的配置开启懒加载后,执行第一次查询操作 实际上 返回的是 通过Cglig或者Javassist创建的代理对象
因此,调用代理对象的Getter方法 获取 懒加载属性时,会执行 动态代理 的 拦截方法
在拦截方法中,通过Getter方法名称 获取 Java实体属性名称,然后根据 属性名称 获取 对应的LoadPair对象
LoadPair对象中维护了Mapper的Id,有了Mapper的Id就可以获取对应的MappedStatement对象
接着执行一次额外的查询操作,使用查询结果为懒加载属性赋值