[Laravel] Đừng lạm dụng load() nữa nếu bạn chưa hiểu loadMissing - Cứu cánh cho những hệ thống lớn
Lời mở đầu: Câu chuyện "Tối Ưu Hóa" đi vào ngõ cụt
Nhớ lại cái thời mới chập chững viết API bằng Laravel, lỗi ám ảnh lớn nhất của anh em mình chắc chắn là N+1 Query. Khắc phục nó dễ dàng: cứ đè with() (Eager Loading) hoặc load() (Lazy Eager Loading) ra mà dùng. Hệ thống chạy mượt, response time giảm cái rụp, sếp vô vai khen ngợi.
Nhưng chuyện đời đâu có như mơ.
Khi dự án scale lên, bussiness logic phình to. Một Model chạy qua dăm bẩy cái Service, qua Event Listener, rồi chui lọt vào Queue JOb. Lúc này bạn thấy trong Log báo về những câu query.... y hệt nhau, lặp đi lặp lại, bạn tự hỏi: "Quái lạ mình đã Eager Load ở Controller rồi cơ mà, sao vào đến job nó lại query thêm phát nữa?"
Đó là lúc bạn nhận ra, bạn đã rơi vào cái bẫy của việc "Over-Loading" (Load lại những thứ đã có sẵn). Và hôm nay tôi sẽ giải quyết bài toán này bằng vũ khí hạng nặng mà ít anh em để ý đến đó là loadMissing.
1. Vấn đề thực tế: Khi load() trở thành kẻ ngáng đường
Giả sử bạn có một hệ thống E-commerce. Bạn có một hàm processOrder trong một Service nào đó. Vì hàm này có thể được gọi từ nhiều nơi (Controller, Console Command, Job), bạn muốn chắc chắn rằng Order model luôn có sẵn relations items và customer để xử lý.
Thế là bạn viết thế này:
class OrderService
{
public function processOrder(Order $order)
{
// Chắc cốp load luôn cho an toàn
$order->load(['items', 'customer']);
// ... xử lý logic tính tiền, gửi email các kiểu
}
}
Nhìn thì có vẻ rất "phòng thủ" và an toàn, đúng không? NHƯNG:
Nếu ở Controller, bạn đã eager load trước khi truyền vào Service:
$order = Order::with(['items', 'customer'])->find($id);
$orderService->processOrder($order);
Hậu quả là: Laravel sẽ thực thi câu query lấy items và customer thêm một lần nữa bên trong Service, mặc dù dữ liệu đã nằm sẵn trong bộ nhớ của Model $order. Nếu hệ thống có traffic lớn, đây là một sự lãng phí tài nguyên database cực kỳ chí mạng.
2. Giải pháp: Sự tinh tế của loadMissing
Để xử lý cục sưng này, nhiều anh em lôi cái hàm relationLoaded() ra để check thủ công bằng if/else, code dài ngoằng và mất thẩm mỹ.
Nhưng Laravel đã cung cấp sẵn một "viên đạn bạc" từ phiên bản 5.5: loadMissing().
Đúng như cái tên của nó, loadMissing chỉ thực hiện query database nếu relation đó chưa được load. Nếu relation đã tồn tại, nó sẽ mỉm cười bỏ qua, không tốn thêm 1 mili-giây nào của database.
Refactor lại đoạn code trên:
class OrderService
{
public function processOrder(Order $order)
{
// Chỉ load nếu chưa có! Rất thanh lịch và tối ưu.
$order->loadMissing(['items', 'customer']);
// ... xử lý logic
}
}
Bây giờ:
- Nếu truyền
$orderchay (chưa load gì): Nó sẽ query lấyitemsvàcustomer. - Nếu truyền
$orderđãwith(['items', 'customer']): Nó không làm gì cả, lấy luôn data đang có. - Nếu
$ordermới chỉwith('items'): Nó sẽ chỉ tạo query để lấy thêmcustomer.
Tuyệt vời chưa?
3. Đào sâu một chút (Dành cho anh em hệ "Tối cổ")
Chúng ta cùng ngó qua Source Code của Laravel một chút để xem nó "làm phép" thế nào. Bên trong trait Illuminate\Database\Eloquent\Concerns\QueriesRelationships, hàm loadMissing được viết cực kỳ ngắn gọn:
public function loadMissing($relations)
{
$relations = is_string($relations) ? func_get_args() : $relations;
foreach ($relations as $key => $value) {
if (is_numeric($key)) {
$key = $value;
}
// Chìa khóa ở đây: Nếu relation đã load, nó sẽ unset đi!
if ($this->relationLoaded($key)) {
unset($relations[$key]);
continue;
}
}
// Cuối cùng mới gọi hàm load() thần thánh
if (count($relations) > 0) {
$this->load($relations);
}
return $this;
}
Như bạn thấy, bản chất nó là một lớp bọc (wrapper) an toàn cho hàm load(), kết hợp việc check $this->relationLoaded($key) mà ta thường hay làm tay.
4. Nested Relations với loadMissing: Chú ý điểm mù
loadMissing rất mạnh, nhưng có một "cái bẫy" khi bạn làm việc với nested relations (relations lồng nhau).
Ví dụ: $order->loadMissing('items.product.category')
- Nếu
$orderđã loaditems, nhưng chưa loadproductcho cácitemsđó. - Laravel sẽ thấy
itemsđã được load ở Model cấp 1 (Order), nên nó sẽ BỎ QUA TOÀN BỘ chuỗiitems.product.category. - Kết quả: bạn gọi
$order->items->first()->productvà ăn ngay lỗi N+1 (hoặc null tùy logic).
Cách khắc phục: Trong các trường hợp cấu trúc lồng sâu phức tạp, nếu không chắc chắn về state của relations cấp dưới, hãy cân nhắc sử dụng load() thẳng tay, hoặc kiểm tra logic cẩn thận hơn thay vì phó mặc hoàn toàn cho loadMissing.
Tóm lại
with(): Dùng khi bạn đang build query gốc.load(): Dùng khi bạn vừa tạo/lấy object lên và MUỐN ÉP nó lấy relations ngay lập tức (Refresh lại relation).loadMissing(): Dùng khi model được truyền qua lại giữa các class/method/job và bạn muốn đảm bảo relation tồn tại mà không sợ query thừa mứa.
Một thay đổi nhỏ trong tư duy dùng hàm cũng đủ để phân biệt một dev biết gõ phím và một kĩ sư thực sự hiểu về tối ưu hóa hệ thống. Cảm ơn anh em đã đọc, nếu thấy hay hãy upvote cho bài viết nhé!
All rights reserved