首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

MyBatis 延遲載入完整指南:association/collection 配置、原理與最佳實踐

     編輯:Java 綜合教學

什麼是延遲載入?

延遲載入 (Lazy Loading) 是一種「按需獲取」策略:

  • 立即載入:查詢訂單時同時 JOIN 查詢用戶、商品等關聯資料 (單次 SQL,但資料量大)
  • 延遲載入:先查訂單主表,只有當程式碼真正存取 order.getUser() 時,才發送第二條 SQL 查詢用戶

核心價值:避免不必要的關聯查詢,減少資料庫負載,提升查詢效能,特別適用於「關聯資料不常被存取」的場景。

MyBatis 延遲載入實作機制

MyBatis 透過 <association> (一對一) 和 <collection> (一對多) 的 select 屬性指定延遲載入的 SQL 陳述式 (Statement ID),配合 column 傳遞關聯鍵值。

完整配置範例:訂單-用戶一對一延遲載入

1. Mapper XML 配置

<mapper namespace="com.example.mapper.OrderMapper">
    
    <!-- 1. 主查詢:僅查訂單基本資料 -->
    <select id="findOrdersLazy" resultMap="orderUserLazyMap">
        SELECT id, user_id, number, createtime, note FROM orders
    </select>

    <!-- 2. 延遲載入查詢:根據 user_id 查用戶 -->
    <select id="findUserById" parameterType="int" resultType="com.example.po.User">
        SELECT id, username, sex, address FROM user WHERE id = #{id}
    </select>

    <!-- 3. ResultMap 定義延遲載入關聯 -->
    <resultMap id="orderUserLazyMap" type="com.example.po.Order">
        <id column="id" property="id" />
        <result column="user_id" property="userId" />
        <result column="number" property="number" />
        <result column="createtime" property="createtime" />
        <result column="note" property="note" />
        
        <!-- 關鍵配置 -->
        <association property="user" 
                     javaType="com.example.po.User" 
                     select="findUserById"      <!-- 延遲載入的 Statement ID -->
                     column="user_id" />        <!-- 傳遞給 select 的參數值 -->
    </resultMap>
</mapper>

2. 實體類別

public class Order {
    private int id;
    private int userId;
    private String number;
    private Date createtime;
    private String note;
    private User user;  // 關聯物件,延遲載入
    // getter/setter...
}

public class User {
    private int id;
    private String username;
    private String sex;
    private String address;
    // getter/setter...
}

3. Mapper 介面

public interface OrderMapper {
    List<Order> findOrdersLazy() throws Exception;
}

4. 全域開啟延遲載入 (MyBatis 核心配置)

<settings>
    <!-- 總開關 -->
    <setting name="lazyLoadingEnabled" value="true" />
    
    <!-- 積極載入 vs 消極載入 -->
    <!-- true: 任意延遲屬性被存取時,載入所有延遲屬性 -->
    <!-- false (推薦): 僅載入被存取的特定屬性 (按需載入) -->
    <setting name="aggressiveLazyLoading" value="false" />
</settings>

5. 測試驗證

@Test
public void testLazyLoading() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    OrderMapper mapper = sqlSession.getMapper(OrderMapper.class);
    
    List<Order> orders = mapper.findOrdersLazy(); // 只查 orders 表
    
    for (Order order : orders) {
        // 此時才觸發用戶查詢
        System.out.println(order.getUser().getUsername()); 
    }
}

6. SQL 執行日誌見證

DEBUG - ==> Preparing: SELECT id, user_id, number, createtime, note FROM orders
DEBUG - ==> Parameters: 
DEBUG - <== Total: 3
DEBUG - ==> Preparing: SELECT id, username, sex, address FROM user WHERE id = ?
DEBUG - ==> Parameters: 1(Integer)
DEBUG - <== Total: 1
... (只有存取 user 時才查詢) ...

collection 一對多延遲載入

<resultMap id="orderDetailLazyMap" type="com.example.po.Order">
    <id column="id" property="id" />
    <result column="user_id" property="userId" />
    <!-- 一對多延遲載入 -->
    <collection property="orderDetails" 
                ofType="com.example.po.OrderDetail"
                select="findOrderDetailsByOrderId"
                column="id" />
</resultMap>

<select id="findOrderDetailsByOrderId" parameterType="int" resultType="com.example.po.OrderDetail">
    SELECT * FROM order_detail WHERE order_id = #{id}
</select>

延遲載入原理深度解析

  1. 動態代理:MyBatis 為結果物件建立代理 (Javassist 或 CGLIB)
  2. 攔截 Getter:當呼叫 order.getUser() 時,代理攔截
  3. 檢查載入狀態:若關聯物件為 null (未載入)
  4. 執行 Select:使用 column 值作參數,執行 select 指定的 Statement
  5. 填充結果:將查詢結果設定到目標屬性,返回

手動實作延遲載入 (不依賴 MyBatis 機制)

// Service 層手動控制
public OrderVO getOrderWithUserLazy(int orderId) {
    Order order = orderMapper.findById(orderId); // 單表查詢
    OrderVO vo = new OrderVO(order);
    
    // 按需載入用戶 (例如前端請求包含 user 欄位時)
    if (needUserInfo) {
        User user = userMapper.findById(order.getUserId());
        vo.setUser(user);
    }
    return vo;
}

優勢:完全可控、無魔法、易除錯。劣勢:樣板代碼多、易忘記載入導致 N+1 問題。

最佳實踐與決策指南

場景建議策略理由
關聯資料「幾乎總是」被使用立即載入 (JOIN 查詢 或 嵌套 ResultMap)減少資料庫往返,單次 SQL 完成
關聯資料「偶爾」被使用延遲載入避免 90% 查詢的無效 JOIN
列表頁面,僅顯示主表欄位延遲載入大幅降低列表查詢負載
詳情頁面,需完整關聯資料立即載入 或 手動批次載入避免 N+1 問題,單次或批次查詢
高併發熱點查詢立即載入 + 快取延遲載入增加鎖競爭與連線佔用

常見坑與避坑指南

  • ❌ N+1 問題:列表遍歷時逐筆觸發延遲載入 → 改用立即載入或批次預載
  • ❌ Session 關閉後存取:延遲載入需在同一 SqlSession 中 → 延長 Session 生命週期或改用立即載入
  • ❌ 序列化失敗:代理物件無法直接序列化 (JSON/RMI) → DTO 轉換層切斷代理
  • ❌ aggressiveLazyLoading=true:存取任一屬性觸發所有延遲載入 → 設為 false
  • ✅ 測試驗證 SQL 數量:使用 P6Spy、druid 監控實際執行 SQL 條數
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved