Mybatis 懶載入使用及源碼分析

来源:https://www.cnblogs.com/redwinter/archive/2022/08/22/16612158.html
-Advertisement-
Play Games

Mybatis 懶載入的使用 什麼是懶載入?懶載入的意思就是在使用的時候才去載入,不使用不去載入,相反的就叫饑餓載入或者立即載入。懶載入在Mybatis中一般是存在與聯合查詢的情況,比如查詢一個對象的同時連帶查詢相關的表對應的數據。在Mybatis中查詢可以通過ResultMap設置查詢對象返回一個 ...


Mybatis 懶載入的使用

什麼是懶載入?懶載入的意思就是在使用的時候才去載入,不使用不去載入,相反的就叫饑餓載入或者立即載入。懶載入在Mybatis中一般是存在與聯合查詢的情況,比如查詢一個對象的同時連帶查詢相關的表對應的數據。在Mybatis中查詢可以通過ResultMap設置查詢對象返回一個集合屬性,也就是說像這樣的:

@Data
public class User implements Serializable {

    private int id;
    private int age;
    private String name;
    private List<Order> orderList;
}

這裡的orderList就是一個集合,在mapper.xml中配置如下:

<resultMap id="userMap" type="mybatis.model.User">
    <id column="id" property="id"/>
    <result property="age" column="age"/>
    <result property="name" column="name"/>
    <collection property="orderList" ofType="mybatis.model.Order" column="id" select="findByUid"/>
</resultMap>

<select id="findByUid" resultType="mybatis.model.Order">
    select * from `order` where uid = #{id}
</select>

<select id="selectById" resultMap="userMap">
    select * from user where id = #{id}
</select>

可以看到這裡查詢User對象的時候還查詢了Order列表,這個用戶關聯的訂單信息。如果只是這樣查詢那麼結果是饑餓載入:

@Test
public void testLazyLoad(){
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user = mapper.selectById(1);
    System.out.println(user.getName());
}

輸出結果,執行了兩個sql語句查詢,說明查詢User的同時也查詢了Order

09:52:56.575 [main] INFO mybatis.plugins.MyPlugin - 對方法進行增強....
==>  Preparing: select * from user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, age, name
<==        Row: 1, 18, 靈犀
Cache Hit Ratio [mybatis.mapper.UserMapper]: 0.0
09:52:56.613 [main] INFO mybatis.plugins.MyPlugin - 對方法進行增強....
====>  Preparing: select * from `order` where uid = ? 
====> Parameters: 1(Integer)
<====    Columns: id, uid, order_name, price
<====        Row: 1, 1, 蘋果, 8.00
<====        Row: 3, 1, 筆記本電腦, 8000.00
<====      Total: 2
<==      Total: 1
靈犀

Process finished with exit code 0

配置懶載入:

<resultMap id="userMap" type="mybatis.model.User">
    <id column="id" property="id"/>
    <result property="age" column="age"/>
    <result property="name" column="name"/>
    <collection property="orderList" ofType="mybatis.model.Order" column="id" select="findByUid" fetchType="lazy"/>
</resultMap>

這裡的collection標簽中的fetchType屬性可以設置為lazy或者eager,預設就是eager饑餓載入,配置完之後執行:

09:56:22.649 [main] INFO mybatis.plugins.MyPlugin - 對方法進行增強....
==>  Preparing: select * from user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, age, name
<==        Row: 1, 18, 靈犀
<==      Total: 1
靈犀

可以看到只執行了查詢usersql語句,而查詢訂單ordersql語句沒有執行,只有在使用orderList這個屬性的時候才會去執行sql查詢:

@Test
public void testLazyLoad(){
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user = mapper.selectById(1);
    System.out.println(user.getName());
    // 懶載入
    System.out.println(user.getOrderList());
}

輸出結果:

09:58:02.681 [main] INFO mybatis.plugins.MyPlugin - 對方法進行增強....
==>  Preparing: select * from user where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, age, name
<==        Row: 1, 18, 靈犀
<==      Total: 1
靈犀
Cache Hit Ratio [mybatis.mapper.UserMapper]: 0.0
09:58:02.746 [main] INFO mybatis.plugins.MyPlugin - 對方法進行增強....
==>  Preparing: select * from `order` where uid = ? 
==> Parameters: 1(Integer)
<==    Columns: id, uid, order_name, price
<==        Row: 1, 1, 蘋果, 8.00
<==        Row: 3, 1, 筆記本電腦, 8000.00
<==      Total: 2
[Order(id=1, uid=1, orderName=蘋果, price=8.00), Order(id=3, uid=1, orderName=筆記本電腦, price=8000.00)]

Process finished with exit code 0

可以看到執行查詢訂單的sql語句並且列印了訂單信息

Mybatis 懶載入原理及源碼解析

Mybatis懶載入的原理要搞清楚的話,就需要去找到返回結果的時候看看Mybatis是如何封裝的,找到ResultSetHandler,因為這個介面就是專門用於結果集封裝的,預設實現為DefaultResultSetHandler,根據查詢數據流程不難發現封裝結果集的時候調用的是handleResultSets方法:

 @Override
  public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

    final List<Object> multipleResults = new ArrayList<>();

    int resultSetCount = 0;
    // 獲取ResultSet的包裝器
    ResultSetWrapper rsw = getFirstResultSet(stmt);

    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    // 驗證結果數量
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
      ResultMap resultMap = resultMaps.get(resultSetCount);
      // 處理結果集
      handleResultSet(rsw, resultMap, multipleResults, null);
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }

點擊處理結果集的方法:

private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
      if (parentMapping != null) {
        // 處理每行的數據
        handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
      } else {
        if (resultHandler == null) {
          DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
          handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
          multipleResults.add(defaultResultHandler.getResultList());
        } else {
          handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
        }
      }
    } finally {
      // issue #228 (close resultsets)
      closeResultSet(rsw.getResultSet());
    }
  }

點擊處理每行的數據方法:

  public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    // 如果存在嵌套的結果集
    if (resultMap.hasNestedResultMaps()) {
      // 安全行約束檢查,如果是嵌套查詢需要關閉安全行約束條件
      ensureNoRowBounds();
      // 檢查結果處理器是否符合嵌套查詢約束
      checkResultHandler();
      // 執行嵌套查詢結果集處理
      handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
      // 簡單的結果集分裝處理
      handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
  }

由於我們寫的這個結果是簡單結果集,所以進入handleRowValuesForSimpleResultMap

private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
      throws SQLException {
    DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
    ResultSet resultSet = rsw.getResultSet();
    skipRows(resultSet, rowBounds);
    while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
      ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
      // 獲取每行的值
      Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
      storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
    }
  }

挑重點,直接進入獲取每行值方法中:

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    // 創建結果值
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
    if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
      final MetaObject metaObject = configuration.newMetaObject(rowValue);
      boolean foundValues = this.useConstructorMappings;
      if (shouldApplyAutomaticMappings(resultMap, false)) {
        foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
      }
      foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
      foundValues = lazyLoader.size() > 0 || foundValues;
      rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
    }
    return rowValue;
  }

繼續進入獲取每行結果值的方法,createResultObject:

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<>();
    // 創建結果對象 ,使用ObjectFactory 反射進行創建
    Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
    if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
      final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
      for (ResultMapping propertyMapping : propertyMappings) {
        // issue gcode #109 && issue #149
        // 檢查屬性是否是懶載入的屬性
        if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
          // 使用動態代理創建一個代理對象作為結果對象返回出去,預設使用javassist 進行創建
          resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
          break;
        }
      }
    }
    this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
    return resultObject;
  }

這裡就先是通過反射創建出這個對象resultObject,然後遍歷去檢查這些屬性是否是懶載入的,如果是那麼就通過代理工廠去創建一個代理對象,由於這裡創建的是一個返回對象,不是一個介面因此動態代理實現是通過cglib實現的,Mybatis這裡使用javassist包下的代理進行創建代理對象,代理工廠預設就是JavassistProxyFactory:

static Object crateProxy(Class<?> type, MethodHandler callback, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {

    ProxyFactory enhancer = new ProxyFactory();
    enhancer.setSuperclass(type);

    try {
      type.getDeclaredMethod(WRITE_REPLACE_METHOD);
      // ObjectOutputStream will call writeReplace of objects returned by writeReplace
      if (LogHolder.log.isDebugEnabled()) {
        LogHolder.log.debug(WRITE_REPLACE_METHOD + " method was found on bean " + type + ", make sure it returns this");
      }
    } catch (NoSuchMethodException e) {
      enhancer.setInterfaces(new Class[] { WriteReplaceInterface.class });
    } catch (SecurityException e) {
      // nothing to do here
    }

    Object enhanced;
    Class<?>[] typesArray = constructorArgTypes.toArray(new Class[constructorArgTypes.size()]);
    Object[] valuesArray = constructorArgs.toArray(new Object[constructorArgs.size()]);
    try {
      // 創建代理對象
      enhanced = enhancer.create(typesArray, valuesArray);
    } catch (Exception e) {
      throw new ExecutorException("Error creating lazy proxy.  Cause: " + e, e);
    }
    ((Proxy) enhanced).setHandler(callback);
    return enhanced;
  }

實際上這裡也是通過反射進行創建,只是在外面封裝成了ProxyFactory這個對象,當我們調用getOrderList方法的時候就會執行到invoke方法中,並且判斷是否是延遲載入的,如果是那麼就會執行lazyLoader.load方法執行延遲載入,也就是執行sql查詢數據:

@Override
    public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
      final String methodName = method.getName();
      try {
        synchronized (lazyLoader) {
          if (WRITE_REPLACE_METHOD.equals(methodName)) {
            Object original;
            if (constructorArgTypes.isEmpty()) {
              original = objectFactory.create(type);
            } else {
              original = objectFactory.create(type, constructorArgTypes, constructorArgs);
            }
            PropertyCopier.copyBeanProperties(type, enhanced, original);
            if (lazyLoader.size() > 0) {
              return new JavassistSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
            } else {
              return original;
            }
          } else {
            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);
                //判斷方法是否是get方法
              } else if (PropertyNamer.isGetter(methodName)) {
                final String property = PropertyNamer.methodToProperty(methodName);
                // 判斷屬性是否是延遲載入的。如果是那麼執行載入
                if (lazyLoader.hasLoader(property)) {
                  lazyLoader.load(property);
                }
              }
            }
          }
        }
        // 執行原方法
        return methodProxy.invoke(enhanced, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
  }

load方法就會執行真正的查詢sql語句,將數據賦值給User對象,這樣就完成了真正的懶載入操作,所以Mybatis的懶載入實際上就是利用動態代理將對象的參數封裝進行了延遲載入,當需要時再去調用真正的查詢操作並返回數據。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 二、流程式控制制 1、用戶交互Scanner java.util.Scanner是Java5的新特性,我們可以通過Scanner類來獲取用戶輸入。 基本語法: Scanner s = new Scanner(System.in); 通過Scanner類的net()與nextLine()方法獲取輸入的字元 ...
  • 1、insert 標簽 1.1 獲取SqlSessionFactory 對象的通用方法 方便後面分測試; //獲取SqlSessionFactory 對象的通用方法 public SqlSessionFactory getSqlSessionFactory() throws IOException ...
  • Base 64 不屬於密碼技術,僅是編碼方式。但由於在 Java、JavaScript、區塊鏈等出現的頻率較高,故在本系列文章中首先分享 Base 64 編碼技術。前面部分主要介紹 Base 64 理論性的內容,如果只看在 Java(SpringBoot)或 JS(Vue)中的實現,可以直接跳到最後... ...
  • Docker安裝 卸載舊版本 較舊的 Docker 版本稱為 docker 或 docker-engine 。如果已安裝這些程式,請卸載它們以及相關的依賴項。 yum remove docker \ docker-client \ docker-client-latest \ docker-comm ...
  • 筆記記錄 B站狂神說Java的ElasticSearch課程:https://www.bilibili.com/video/BV17a4y1x7zq 一、ElasticSearch概述 官網:https://www.elastic.co/cn/downloads/elasticsearch Elat ...
  • 前言 嗨嘍,大家好呀~這裡是愛看美女的茜茜吶 我們在做採集數據的時候,過快或者訪問頻繁,或者一訪問就給彈出驗證碼,然後就蚌珠了~ 那麼今天!博主就給大家來一個簡單處理驗證碼的方法 環境模塊 Python和pycharm如果還有小伙伴沒安裝的話,可以在文章最下方掃碼獲取安裝包。 這裡需要用到一個 dd ...
  • 前期需要儲備的知識點 併發 看起來同時運行的就可以稱之為併發,其實內部是做了0.1秒A,做了0.1秒B,交替進行運作,看起來像是一起運作的。 並行 真正意義上的同時執行 補充 1.並行肯定算是併發 2.單核的電腦肯定不能實現並行,但是可以實現併發!! 3.我們這裡的單核是假設就是一個核,幹活的就一 ...
  • Swagger以及knife4j基本使用 Swagger 介紹: 官網:https://swagger.io/ Swagger 是一個規範和完整的框架,用於生成、描述、調用和可視化RESTful風格的 Web 服務 Restful 面向資源 RESTful是一種架構的規範與約束、原則,符合這種規範的 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...