Tóm tắt: Khi làm việc với Eloquent Relationships, việc load dữ liệu liên quan đúng cách là chìa khoá để tránh N+1 query và tối ưu hiệu năng. Bài viết này phân tích sâu 3 phương thức
with(),load()vàloadMissing()— khi nào dùng, cách hoạt động, và edge case cần lưu ý.
Bối cảnh
Giả sử bạn có 3 model: Order, User, OrderItem với relationships:
class Order extends Model {
public function user() { return $this->belongsTo(User::class); }
public function items() { return $this->hasMany(OrderItem::class); }
}
Bạn cần truy cập user và items của nhiều orders. Nếu không eager load, mỗi lần truy cập relationship sẽ phát sinh thêm 1 query → N+1 problem. Laravel cung cấp 3 phương thức chính để giải quyết vấn đề này.
1. with() — Eager Load ngay lúc query
Khi nào dùng: Bạn biết chắc sẽ cần relationship ngay từ đầu.
Cách hoạt động: Laravel chạy thêm 1 query riêng cho relationship cùng lúc với query chính.
// 2 queries — bất kể có 10 hay 10.000 orders
$orders = Order::with('user')->get();
// Query 1: SELECT * FROM orders
// Query 2: SELECT * FROM users WHERE id IN (1, 2, 3, ...)
foreach ($orders as $order) {
echo $order->user->name; // Không phát sinh query — đã load sẵn
}
Các pattern nâng cao
Load nhiều relationships cùng lúc:
$orders = Order::with(['user', 'items'])->get();
// → 3 queries: orders + users + order_items
Load nested relationship:
$orders = Order::with('items.product')->get();
// → 3 queries: orders + order_items + products
Constrained eager loading (load kèm điều kiện):
$orders = Order::with(['items' => function ($query) {
$query->where('quantity', '>', 1)
->orderBy('price', 'desc');
}])->get();
// → Chỉ load items có quantity > 1, sắp theo giá giảm dần
2. load() — Lazy Eager Load sau khi đã có model
Khi nào dùng: Bạn đã có model/collection rồi, nhưng sau đó mới cần thêm relationship — thường gặp trong logic phân nhánh hoặc dynamic include từ API.
Cách hoạt động: Giống with() về mặt query, nhưng gọi sau khi đã có collection.
$orders = Order::all(); // Query 1: SELECT * FROM orders
if ($request->needsUserInfo) {
$orders->load('user'); // Query 2: SELECT * FROM users WHERE id IN (...)
}
if ($request->needsItems) {
$orders->load('items'); // Query 3: SELECT * FROM order_items WHERE order_id IN (...)
}
Ví dụ thực tế — API controller với dynamic include
public function index(Request $request)
{
$orders = Order::where('status', 'pending')->get();
// Client gửi ?include=user,items → load theo yêu cầu
if ($request->has('include')) {
$includes = explode(',', $request->include); // ['user', 'items']
$orders->load($includes);
}
return OrderResource::collection($orders);
}
Dùng trên single model
$order = Order::find(1); // 1 query
$order->load('user', 'items'); // 2 queries thêm
// Tổng = 3 queries — tốt hơn N+1
3. loadMissing() — Chỉ load nếu chưa có
Khi nào dùng: Code phức tạp, relationship có thể đã được load trước đó bởi một phần code khác → tránh query trùng lặp.
Cách hoạt động: Kiểm tra relationship đã load chưa → chỉ query nếu chưa có.
$orders = Order::with('user')->get(); // Đã load 'user'
$orders->loadMissing('user'); // ❌ KHÔNG chạy query — vì 'user' đã có
$orders->loadMissing('items'); // ✅ Chạy query — vì 'items' chưa load
// So sánh: nếu dùng load()
$orders->load('user'); // ⚠️ Chạy lại query user — LÃNG PHÍ!
$orders->load('items'); // ✅ Chạy query items
Ví dụ thực tế — Service layer an toàn
class OrderExportService
{
public function export(Collection $orders): array
{
// Không biết caller đã load gì → dùng loadMissing an toàn
$orders->loadMissing(['user', 'items.product']);
return $orders->map(function ($order) {
return [
'order_id' => $order->id,
'customer' => $order->user->name,
'items' => $order->items->map(fn ($i) => [
'product' => $i->product->name,
'qty' => $i->quantity,
]),
];
})->toArray();
}
}
Caller 1 — đã load user từ trước:
$orders = Order::with('user')->where('status', 'completed')->get();
$exportService->export($orders);
// → loadMissing chỉ load 'items.product', skip 'user' ✅
Caller 2 — chưa load gì:
$orders = Order::where('date', today())->get();
$exportService->export($orders);
// → loadMissing load cả 'user' + 'items.product' ✅
Bảng so sánh tổng hợp
with() |
load() |
loadMissing() |
|
|---|---|---|---|
| Thời điểm gọi | Lúc query (trước get()) |
Sau khi có model/collection | Sau khi có model/collection |
| Load trùng? | — | ⚠️ Có — chạy lại query | ✅ Không — skip nếu đã có |
| Use case chính | Biết trước cần gì | Logic phân nhánh, dynamic include | Service/helper nhận data từ nhiều nguồn |
| Syntax | Model::with('rel')->get() |
$collection->load('rel') |
$collection->loadMissing('rel') |
Edge Case: Khi loadMissing() không phù hợp
loadMissing() kiểm tra: "relationship này đã load chưa?" → Nếu đã có (dù data cũ) → skip. Nó không biết data đã thay đổi trong DB.
$order = Order::with('items')->find(1);
// items = [Item A, Item B]
// ... Thêm item mới vào order ...
OrderItem::create(['order_id' => $order->id, 'product_id' => 99]);
$order->loadMissing('items');
$order->items; // ❌ Vẫn [Item A, Item B] — THIẾU item mới!
$order->load('items');
$order->items; // ✅ [Item A, Item B, Item C] — đúng!
💡 Khi bạn vừa tạo/sửa/xoá related records và cần đọc lại dữ liệu mới nhất → dùng
load()để force refresh.
Nguyên tắc chọn nhanh
| Tình huống | Chọn |
|---|---|
| Biết trước cần relationship nào → load cùng query | with() ✅ |
| Đã có model, cần load thêm theo logic/điều kiện | load() ✅ |
| Không kiểm soát được input đã load gì → tránh query trùng | loadMissing() ✅ |
| Data có thể đã thay đổi → cần refresh từ DB | load() ✅ |
Kết luận
- Mặc định dùng
with()— phổ biến nhất, rõ ràng nhất, giải quyết N+1 từ gốc. - Dùng
load()khi logic phân nhánh, dynamic include từ API request, hoặc cần refresh data. - Dùng
loadMissing()trong shared service/helper mà không kiểm soát được input đã load gì. - ~95% trường hợp, data không thay đổi giữa chừng trong cùng 1 request →
loadMissing()an toàn hơnload(). Chỉ khi bạn vừa mutate related records → dùngload()để force refresh.