0

Laravel preventLazyLoading(): "Cảnh Sát Giao Thông" Bắt Lỗi N+1 Query Ngay Từ Trong Trứng Nước

Chào anh em! Trong suốt nhiều năm làm Backend, từ những hệ thống nhỏ lẻ đến những dự án e-commerce lớn chịu tải cao, tôi nhận ra một sự thật đau lòng: 90% nguyên nhân làm ứng dụng Laravel chậm đi không nằm ở code PHP, mà nằm ở cách chúng ta giao tiếp với Database.

Và kẻ sát nhân thầm lặng khét tiếng nhất chính là N+1 Query.

Hôm nay, chúng ta sẽ không nói lý thuyết suông về Eager Loading nữa, mà sẽ bàn về một "vũ khí tối thượng" được Taylor Otwell giới thiệu từ Laravel 8: method Model::preventLazyLoading(). Tại sao tôi lại gọi nó là vũ khí tối thượng? Hãy cùng đi vào thực chiến.

1. N+1 Query: Kẻ sát nhân thầm lặng (Và cú lừa của Localhost)

Hãy xem xét một đoạn code mà bất kỳ anh em Fresher/Junior nào cũng từng viết:

// Controller
$users = User::all();

// View (Blade) hoặc API Resource
foreach ($users as $user) {
    echo $user->profile->address; 
}

Trên máy dev của bạn (Localhost), với database có vỏn vẹn 10 user, đoạn code này chạy mượt như lụa. Bạn tự tin push code lên production.

Nhưng khi production có 10,000 users, hệ thống sập. Tại sao?

Vì tính năng Lazy Loading (tải lười biếng) của Eloquent. Khi bạn gọi $user->profile, Laravel ngầm hiểu: "À, lúc nãy chưa lấy profile, giờ mình sẽ chạy thêm 1 câu query để lấy nó".

Kết quả:

  • 1 câu query để lấy ra danh sách Users.
  • N câu query để lấy Profile cho từng User trong vòng lặp. -> Tổng cộng là N+1 queries. 10,000 users = 10,001 câu truy vấn chọc vào Database cùng một lúc. Chết server là cái chắc!

Cách giải quyết truyền thống là dùng Eager Loading (gọi trước bằng with()):

$users = User::with('profile')->get();

Nhưng con người thì luôn có lúc sai sót. Làm sao để hệ thống tự động chửi chúng ta mỗi khi ta lỡ quên with()? Đó là lúc preventLazyLoading() xuất hiện.

2. preventLazyLoading() là gì?

Nói một cách dễ hiểu, preventLazyLoading() là một chế độ "Strict Mode" (nghiêm ngặt) dành cho Eloquent Model.

Khi bạn bật tính năng này, bất cứ khi nào code của bạn vô tình kích hoạt Lazy Loading (tạo ra N+1 query), Laravel sẽ không âm thầm chạy câu query đó nữa. Thay vào đó, nó sẽ quăng thẳng một cái Exception (lỗi LazyLoadingViolationException) vào mặt bạn ngay lập tức.

Code của bạn sẽ bị "chết" ngay trên localhost. Nhờ cú tát cảnh tỉnh này, bạn buộc phải sửa lại logic, thêm with() vào trước khi tính năng đó có cơ hội lên môi trường Production.

3. Cấu hình "Chuẩn Senior" trong dự án thực tế

Việc bật tính năng này cực kỳ đơn giản. Bạn chỉ cần vào file app/Providers/AppServiceProvider.php và thêm vào method boot():

use Illuminate\Database\Eloquent\Model;

public function boot()
{
    // Bật chế độ chống N+1 query
    Model::preventLazyLoading(! $this->app->isProduction());
}

Tại sao lại phải có điều kiện ! $this->app->isProduction()? Đây là kinh nghiệm xương máu nhé anh em!

  • Ở môi trường Local / Testing, chúng ta MUỐN code bị lỗi tung tóe (HTTP 500) để team Dev biết ngay mà fix.
  • Nhưng ở môi trường Production, thà hệ thống load chậm đi một chút (N+1) còn hơn là quăng lỗi 500 trắng trang vào mặt khách hàng đang mua sắm. Việc truyền điều kiện này vào đảm bảo rằng tính năng cảnh sát này chỉ hoạt động ở nhà, ra đường thì nó sẽ nhắm mắt làm ngơ để bảo vệ trải nghiệm người dùng.

4. Tại sao 100% dự án mới nên bật tính năng này?

Từ khi biết đến preventLazyLoading(), tôi luôn đưa nó vào quy chuẩn setup cho mọi dự án Laravel mới ngay từ Day 1, vì những lợi ích khổng lồ:

  1. Shift-Left Testing: Phát hiện lỗi hiệu năng ngay từ khâu gõ code, thay vì chờ đến lúc QA test hoặc khách hàng phàn nàn.
  2. Ép Dev mới vào khuôn khổ: Khi team có các bạn mới chưa quen với Eager Loading, tính năng này giống như một ông Tech Lead ngồi cạnh review code auto 24/7. Cứ quên with() là màn hình đỏ lòm.
  3. Tiết kiệm thời gian trace bug: Bạn không cần phải mở Laravel Telescope hay Clockwork lên để soi từng câu query xem chỗ nào bị N+1 nữa. Lỗi ở dòng nào, nó báo chính xác dòng đó.

Lời kết

Database thường là nút thắt cổ chai lớn nhất của mọi hệ thống Backend. Thay vì tốn hàng ngàn đô la để nâng cấp RAM/CPU cho máy chủ chứa Database, hãy làm tốt việc tối ưu Query ngay từ trong source code.

Hãy bật Model::preventLazyLoading(! app()->isProduction()); ngay hôm nay, và tin tôi đi, một năm sau nhìn lại, bạn sẽ tự cảm ơn chính mình!

Bạn thấy cấu trúc này ổn chứ? Phần giải thích tại sao chỉ bật ở môi trường Local/Testing là điểm nhấn kinh nghiệm thực tế rất quan trọng khi đi làm dự án lớn, giúp bài viết của bạn có giá trị thực tiễn cao hơn hẳn những bài dịch document thông thường.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí