首頁 > 軟體

一文了解mybatis的延遲載入

2022-07-15 10:00:35

本文主要介紹下mybatis的延遲載入,從原理上介紹下怎麼使用、有什麼好處能規避什麼問題。延遲載入一般用於級聯查詢(級聯查詢可以將主表不能直接查詢的資料使用自定義對映規則呼叫字表來查,主查詢查完之後通過某個column列或多個列將查詢結果傳遞給子查詢,子查詢再根據主查詢傳遞的引數進行查詢,最後將子查詢結果進行對映)。mybatis的懶載入是通過建立代理物件來實現的,只有當呼叫getter等方法的時候才會去查詢子查詢,查詢後完成設值再獲取值。

1. 什麼時候會建立代理物件

  private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
    this.useConstructorMappings = false; // reset previous mapping result
    final List<Class<?>> constructorArgTypes = new ArrayList<>();
    final List<Object> constructorArgs = new ArrayList<>();
    // 建立result接收物件
    Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
    if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
      // 處理其他屬性properties
      final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
      for (ResultMapping propertyMapping : propertyMappings) {
        // issue gcode #109 && issue #149 建立代理
        if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
          resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
          break;
        }
      }
    }
    // 使用有參建構函式建立了物件
    this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
    return resultObject;
  }

通過mybatis程式碼propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()發現只有當存在巢狀查詢select子句和isLazy=true的時候才會建立代理,那麼isLazy=true是什麼條件,從建立ResultMapping的程式碼中可以看到boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));只有手動設定fetchType=lazy或者全域性設定configuration的lazyLoadingEnabled=true,兩者缺一不可。

關於代理是通過Javassist建立的,下面有一個簡單的例子

public class HelloMethodHandler implements MethodHandler {

  private Object target;

  public HelloMethodHandler(Object o) {
    this.target = o;
  }

  @Override
  public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args) throws Throwable {
    String methodName = thisMethod.getName();
    if (methodName.startsWith("get")) {
      System.out.println("select database....");
      // 進行sql查詢到結果並set設定值
      ((Student)self).setName("monian");
    }
    return proceed.invoke(self, args);
  }

  public static void main(String[] args) throws Exception {
    Student student = new Student();
    ProxyFactory proxyFactory = new ProxyFactory();
    proxyFactory.setSuperclass(Student.class);

    Constructor<Student> declaredConstructor = Student.class.getDeclaredConstructor();
    Object o = proxyFactory.create(declaredConstructor.getParameterTypes(), new Object[]{});
    ((Proxy)o).setHandler(new HelloMethodHandler(student));

    Student proxy = (Student)o;
    System.out.println(proxy.getName());
  }
}

mybatis的原理就是通過建立一個代理物件,當通過這個代理物件呼叫getter、is、equals、clone、toString、hashCode等方法時會呼叫select子查詢,然後完成設定,最後取值就像早就獲取到一樣。

2. 如何使用

public class UserDO {

  private Integer userId;

  private String username;

  private String password;

  private String nickname;

  private List<PermitDO> permitDOList;

  public UserDO() {}
}
<resultMap id="BaseMap" type="org.apache.ibatis.study.entity.UserDO">
    <id column="user_id" jdbcType="INTEGER" property="userId" />
    <result column="username" jdbcType="VARCHAR" property="username" />
    <result column="password" jdbcType="VARCHAR" property="password" />
    <result column="nickname" jdbcType="VARCHAR" property="nickname"/>

    <collection property="permitDOList" column="user_id" select="getPermitsByUserId"
     fetchType="lazy">

    </collection>
</resultMap>

  <resultMap id="PermitBaseMap" type="org.apache.ibatis.study.entity.PermitDO">
    <id column="id" jdbcType="INTEGER" property="id"/>
    <result column="code" jdbcType="VARCHAR" property="code"/>
    <result column="name" jdbcType="VARCHAR" property="name"/>
    <result column="type" jdbcType="TINYINT" property="type"/>
    <result column="pid" jdbcType="INTEGER" property="pid"/>
  </resultMap>
  
      
   <select id="getByUserId2" resultMap="BaseMap">
    select * from user
    where user_id = #{userId}
  </select>

  <select id="getPermitsByUserId" resultMap="PermitBaseMap">
    select p.*
    from user_permit up
    inner join permit p on up.permit_id = p.id
    where up.user_id = #{userId}
  </select>

通過fetchType=lazy指定子查詢getPermitsByUserId使用懶載入,這樣的話就不用管全域性設定lazyLoadingEnabled是true還是false了。當然這裡可以直接用多表關聯查詢不使用子查詢,使用方法在上一篇文章

測試程式碼

public class Test {

  public static void main(String[] args) throws IOException {

    try (InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml")) {
      // 構建session工廠 DefaultSqlSessionFactory
      SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
      SqlSession sqlSession = sqlSessionFactory.openSession();
      UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
      UserDO userDO = userMapper.getByUserId2(1);
      System.out.println(userDO);
    }
  }

}

結果如下,打了斷點可以看到原userDO物件已被代理並且permitDOList是null需要呼叫get方法才會去查詢拿到值,咳咳這邊之前直接執行顯示是已經把permitDOList查詢出來了,想了半天啥原因後來才發現println會呼叫userDO物件的toString方法,而toString方法也會走代理方法直接去呼叫子查詢的

3.延遲載入的好處

延遲載入主要能解決mybatis的N+1問題,什麼是N+1問題其實叫1+N更為合理,以上面的業務例子來說就是假設一次查詢出來10000個使用者,那麼還需要針對這10000個使用者使用子查詢getPermitsByUserId獲取每個使用者的許可權列表,需要10000次查詢,總共10001次,真實情況下你可能並不需要每個子查詢的結果,這樣就浪費資料庫連線資源了。如果使用延遲載入的話就相當於不用進行這10000次查詢,因為它是等到你真正使用的時候才會呼叫子查詢獲取結果。

以上就是一文了解mybatis的延遲載入的詳細內容,更多關於mybatis延遲載入的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com