延遲載入 (Lazy Loading) 是一種「按需獲取」策略:
order.getUser() 時,才發送第二條 SQL 查詢用戶核心價值:避免不必要的關聯查詢,減少資料庫負載,提升查詢效能,特別適用於「關聯資料不常被存取」的場景。
MyBatis 透過 <association> (一對一) 和 <collection> (一對多) 的 select 屬性指定延遲載入的 SQL 陳述式 (Statement ID),配合 column 傳遞關聯鍵值。
<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>
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...
}
public interface OrderMapper {
List<Order> findOrdersLazy() throws Exception;
}
<settings>
<!-- 總開關 -->
<setting name="lazyLoadingEnabled" value="true" />
<!-- 積極載入 vs 消極載入 -->
<!-- true: 任意延遲屬性被存取時,載入所有延遲屬性 -->
<!-- false (推薦): 僅載入被存取的特定屬性 (按需載入) -->
<setting name="aggressiveLazyLoading" value="false" />
</settings>
@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());
}
}
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 時才查詢) ...
<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>
order.getUser() 時,代理攔截column 值作參數,執行 select 指定的 Statement// 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 問題,單次或批次查詢 |
| 高併發熱點查詢 | 立即載入 + 快取 | 延遲載入增加鎖競爭與連線佔用 |
false