Hiểu rõ with(), load() và loadMissing() trong Laravel Eloquent

6 phút đọc

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()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 useritems 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ơn load(). Chỉ khi bạn vừa mutate related records → dùng load() để force refresh.

Bài viết liên quan

Đang cập nhật...