MyBatis 懒加载实战+原理 结果集解析过程(3)

734 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 11 天,点击查看活动详情

阅读本文前需要熟悉结果集解析过程——见上两篇

mybatis版本3.5.12;文章分为两部分

  1. 懒加载应用示例
  2. 懒加载的底层原理(代理模式)

懒加载示例

业务场景:假设我们有一个用户实体User,订单实体Order,一个用户可以关联多个订单对象。实体类如下

User.java

@Data
public class User implements Serializable {
    private Integer id;
    private String username;
    private Integer age;
    private String password;
    private String birthday;
    private Map author;
    private List<Order> order;
}

Order.java

@Data
public class Order implements Serializable {
  private Integer id;
  private Integer uid;
  private String orderName;
}

假设此时有场景:查询指定用户信息,但是不需要订单信息。只有在用户进行调用order字段的时候再进行查询订单信息。这就需要用到MyBatis提供的懒加载功能。

Mapper接口如下

List<User> queryUserLazy();

mapper.xml配置如下

<resultMap id="lazyMap1" type="user" autoMapping="true">
    <!--关闭自动映射,那么没有指定的列名不会出现在结果集中-->
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <result property="birthday" column="birthday"/>
    <result property="password" column="password"/>
    <collection property="order" ofType="order" fetchType="lazy" select="selectOrderLazy" column="id"/>
</resultMap>
<select id="queryUserLazy" resultMap="lazyMap1">
    select * from user limit 2
</select>
<resultMap id="orderLazyMap" type="map">
    <result property="uid" column="uid"/>
    <result property="orderName" column="orderName"/>
</resultMap>
<select id="selectOrderLazy" resultMap="orderLazyMap">
    select * from `order` where uid = #{id}
</select>

测试代码

UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> users = mapper.queryUserLazy();

最后我们查看控制台打印日志信息,发现此时只执行了一条SQL,并没有去加载订单的信息。

image.png

假设我们获取下User的order属性。测试代码稍微修改下:

UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> users = mapper.queryUserLazy();
users.get(0).getOrder(); // 访问下order属性即可

image.png

可以看到此时执行了两条SQL,同时也把用户关联的订单信息也查询出。下面就来说一下懒加载的原理。

原理解析

懒加载的源码分析需要从mybatis解析结果集开始。在开始前,我们必须了解以下组件

  1. ResultLoader
  2. ResultLoaderMap

ResultLoader

每一个懒加载的属性都对应一个ResultLoader对象。当访问实体中的懒加载时,就会调用ResultLoader#loadResult方法去加载这个属性的值。我们先来看下ResultLoader的重要字段信息

public class ResultLoader {
  protected final Configuration configuration;
  protected final Executor executor; // 执行器,执行SQL
  protected final MappedStatement mappedStatement; // 懒加载属性对应的MS对象
  protected final Object parameterObject; // 用户传递的参数
  protected final Class<?> targetType; // 目标Java类型
  protected final ObjectFactory objectFactory;
  protected final CacheKey cacheKey;
  protected final BoundSql boundSql; // 这个属性对应的SQL。
  protected final ResultExtractor resultExtractor; // 结果提取器,把执行器执行sql得到后的结果用它来转换为属性对应的Java类型
  protected final long creatorThreadId;

  protected boolean loaded;
  protected Object resultObject; 
}

现在我们思考下,懒加载属性的值怎么来的呢?肯定是通过某个SQL呀。所以在ResultLoader中只要有SQL信息、懒加载属性的返回值类型、SQL执行器、用户参数。就可以得到懒加载属性最终的值。至于什么时候生成值那就取决于用户了。但是ResultLoader有这个能力得到最后的对象,只不过很懒。这就是懒加载的原理。

接下来,如果用户真的访问了懒加载属性,那就需要查询数据库了。逻辑在loadResult方法里,接下来我们来看loadResult方法(省略核心逻辑)

public Object loadResult() throws SQLException {
  List<Object> list = selectList();
  resultObject = resultExtractor.extractObjectFromList(list, targetType);
  return resultObject;
}

private <E> List<E> selectList() throws SQLException {
   return executor.query(mappedStatement, parameterObject, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER, cacheKey, boundSql);
}

可以看到loadResult的执行流程也很简单,首先,通过执行器调用query方法,把用户参数、ms对象、sql等信息传递过去,查询出结果集。然后再调用extractObjectFromList方法把结果集转换为用户需要的目标对象即可。可以结合下图来理解下一个懒加载属性与ResultLoader的关系。

image.png

ResultLoaderMap

顾名思义,ResultLoaderMap中存放的是ResultLoader的集合。想象一下,一个懒加载属性被封装成一个ResultLoader对象,加入上面说的User实体中还有别的懒加载属性呢?也会被封装成一个ResultLoader对象。那么User实体就对应两个ResultLoader对象,这两个ResultLoader都会被存入到ResultLoaderMap集合中。我们来看一下它的重要属性

public class ResultLoaderMap {
  private final Map<String, LoadPair> loaderMap = new HashMap<>();
}

它只有一个loaderMap对象,它的key值就是属性名,value是一个LoadPair对象,LoadPair其实理解为一个ResultLoader对象。

在ResultLoaderMap中还提供了addLoader方法——添加一个ResultLoader到map中,loadAll方法——遍历map执行所有ResultLoader对象的loadResult方法;load(String property)方法——执行指定属性的ResultLoader的loadResult方法。其中代码简单易懂,就不再贴出来了。

懒加载时机

了解了什么是ResultLoader和ResultLoaderMap之后,我们再来说一下懒加载的时机。懒加载在不同情况下触发的时机是不一样的。分为以下几种情况

  1. resultMap标签内部使用constructor标签定义构造方法,这种情况,无论是否配置懒加载都会在初始化的时候把属性全部加载完毕,也就是说懒加载会失效。
  2. 用户的查询SQL执行结果包含懒加载属性的信息。此时懒加载也会失效。因为用户希望立即加载订单数据。
  3. 只有在示例的情况下懒加载才会生效。原理就是在生成结果集对象的时候会为懒加载属性生成代理对象。所以访问懒加载的属性时mybatis底层执行的就是代理对象的方法了,此时在代理对象内部会调用执行SQL的逻辑。

构造函数导致懒加载失效

在处理结果集时,会首先生成一个空壳对象代码如下,生成空客对象时如果有constructor标签就是用构造方法生成。而最终会走到下面这个逻辑中.

private Object getNestedQueryConstructorValue(ResultSet rs, ResultMapping constructorMapping, String columnPrefix) throws SQLException {
  final String nestedQueryId = constructorMapping.getNestedQueryId();
  final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
  final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
  final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, constructorMapping, nestedQueryParameterType, columnPrefix);
  Object value = null;
  if (nestedQueryParameterObject != null) {
    final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
    final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);
    final Class<?> targetType = constructorMapping.getJavaType();
    final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
    value = resultLoader.loadResult();
  }
  return value;
}

这个逻辑中只要有嵌套的懒加载对象,就会调用resultLoader.loadResult方法为属性赋值,所以懒加载就会失效!。

正常情况下的懒加载

正常情况下的懒加载原理在结果集映射属性的时候为懒加载对象创建代理对象。它的处理逻辑在createResultObject创建空壳对象时,判断是否有嵌套映射并且是懒加载,如果是,就创建代理对象。

for (ResultMapping propertyMapping : propertyMappings) {
  // 如果有嵌套映射,并且是懒加载 并通过 objectFactory 工厂创建对象
  if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
    resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
    break;
  }
}

而代理对象默认使用Javaassit生成的,最终如果访问代理对象会走到JavassistProxyFactory的invoke方法中,在invoke方法会执行ResultLoaderMap对象的laod方法加载指定属性。

public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
    if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
      lazyLoader.loadAll();
    } else if (PropertyNamer.isSetter(methodName)) {
      final String property = PropertyNamer.methodToProperty(methodName);
      lazyLoader.remove(property);
    } else if (PropertyNamer.isGetter(methodName)) {
      final String property = PropertyNamer.methodToProperty(methodName);
      if (lazyLoader.hasLoader(property)) {
        lazyLoader.load(property);
      }
    }
  }
  return methodProxy.invoke(enhanced, args);
}