N+1 trong laravel là gì? Nguyên nhân và cách khắc phục

N+1 là cái quái gì? Làm sao để trị khứa này? Method hay Properties? khi nào thì sử dụng method, khi nào sử dụng properties? Rồi Eloquent với Query builder khác nhau ở đâu, dùng chúng khi nào?…. Chắc hẳn nếu anh em nào đã từng code qua Laravel thì ít nhiều cũng có trong đầu vài câu hỏi như này đúng không?

Hồi mới code laravel mình cũng tự hỏi là thằng cha này hay ho ở chỗ nào mà nhiều ông nghiện nó thế nhỉ? =)). Sau code laravel được 1 thời gian thì mình cũng hiểu tại sao 1 thằng ranh con sinh sau đẻ muộn như Laravel lại không sợ bọn từ lớp 1 đến lớp 5 rồi =)) – Lí do tại sao thì cùng theo dõi hết seri này các bạn sẽ hiểu thôi.

Nhân cái dịp mình đang học đòi làm SEO thì thôi cũng tập tành viết blog để ghi lại những gì mình đã được học và trải nghiệm. Vừa để sau này quên thì đọc lại, vừa biết đâu lại giúp được anh em nào đấy tìm được chân ái của đời mình :v.

Chém gió thế thôi, seri này mình dự định sẽ chia làm 5 phần chính, chia sẻ chủ yếu về laravel core nên ae nào muốn tìm hiểu thêm về laravel cơ bản có thể tham khảo tại trang chủ của laravel nhé.

Nội dung của seri:

  1. Những khái niệm quan trọng cần nắm vững – Recipes & Best Practices.
  2. Service Container – 4 Phương pháp Binding thường dùng
  3. Service Provider – Dùng như nào cho chuẩn
  4. Facade là cái quần què gì – Cơ chế hoạt động của Facade – Thứ mà không 1 IDE nào hiểu được.
  5. Contract là gì? – Tưởng không quen mà quen không tưởng.

Ô KÊ LẸT GÔ…!

N+1 Là gì?

N+1 là gì?

Chắc hẳn anh em code laravel cũng từng 1 vài lần nghe đến thuật ngữ (N+1) rồi đúng không. Nội dung sau đây sẽ giải thích N+1 là gì và làm sao để hạ gục khứa này trong 1 nốt nhạc.

Giả sử bạn có 1 danh sách các bài viết trong bảng Posts với 1 model Eloquent tên là Post. Mỗi Post sẽ thuộc về 1 tác giả (Author) và 1 tác giả có thể có nhiều bài viết (Post). Có nghĩa là Post sẽ có quan hệ belongsTo với Author và Author có quan hệ hasMany với Post.

Nếu bạn chưa hiểu quan hệ BelongsTo và HasMany là gì thì có thể xem bài viết dưới đây. (Link chưa được cập nhật)

Và rồi vào 1 ngày đẹp trời, ông sếp giao cho bạn làm 1 trang PostList cần hiển thị ra danh sách các bài viết và tên tác giả của bài viết đó. Và dưới đây là 1 cách giải quyết thường gặp.

// Bên trong Post Model sẽ định nghĩa mối quan hệ với Author.
public function author()
{
    return $this->belongsTo(Author::class);
}
// ở trong controller hoặc ở trong service sẽ lấy ra all post.
$posts = Post::all();
// Khi ở view sẽ sử dụng vòng lặp để lấy ra thông tin tác giả
foreach ($posts as $post) {
    {{ $post->title }}
    {{ $post->author->name }}
}

Giải thích qua logic của đoạn code bên trên: Ban đầu ta sẽ lấy danh sách bài viết ra, sau đó dùng vòng lặp để lấy thông tin tác giả tương ứng với từng bài viết bằng cách sử dụng đến quan hệ author mà ta đã định nghĩa trong Post eloquent.

Thoạt nhìn thì đọan code bên trên khá là ổn áp đúng không. Tuy nhiên nếu bạn cài Laravel Debugbar thì bạn sẽ nhận ra những vấn đề của cách triển khai này.

Vấn đề 1: Với mỗi 1 bài viết ta sẽ bị mất thêm 1 câu query để lấy ra thông tin của tác giả.

Giả sử trong DB có 5 bài post của cùng 1 tác giả thì đoạn code bên trên sẽ thực thi các lệnh sql như sau:

lệnh Post::all() => SELECT * FROM posts; // giả sử cả 5 bài post đều có author_id = 1
Lệnh trong vòng for =>
Vòng lặp thứ 1: SELECT * FROM author WHERE id = 1;
Vòng lặp thứ 2: SELECT * FROM author WHERE id = 1;
...
Vòng lặp thứ 5: SELECT * FROM author WHERE id = 1;

=> Vậy nếu số lượng bài viết là lớn thì số lượng câu quy vấn sql phải thực thi sẽ là lớn. Chúng ta sẽ mất 1 câu sql để lấy ra N bài viết và N câu sql để lấy ra thông tin tác giả tương ứng với N bài viết đó. Như vậy tổng cộng chúng ta sẽ mất N+1 câu sql, tới đây các bạn đã hiểu tại sao mọi người lại gọi là N + 1 rồi chứ?.

Vấn đề 2: Nếu như 1 tác giả viết nhiều bài viết, như trong ví dụ trên thì ta cùng lấy tác giả có Id = 1 nhưng phải chạy tận 5 câu sql. Điều này dẫn đến trùng lặp query => bất cập vãi chưởng.

Chúng ta đều biết là tác vụ truy vấn SQL là thứ tốn nhiều thời gian xử lý nhất của 1 xử lý request. Rõ ràng việc phải thực hiện 1 đống query chỉ để lấy thêm thông tin Author là 1 việc làm ngu ngok và kém hiệu quả.

Rõ ràng cách code như trên quá bất ổn, phải có giải pháp nào cho việc này chứ nhỉ? Thật may là laravel cung cấp 1 phương pháp để xử gọn thằng này trong vòng 1 nốt nhạc đó là Eager Loading.

Eager Loading: Giải pháp cho vấn đề N+1

eager loading - Giải pháp cho N+1

Thay vì xử lý như đoạn code trước đó thì bây giờ mình sẽ xử lý lại logic mà ông sếp đã giao cho bạn như sau:

// Eager Loading
$posts = Post::with('author')->all();
// Lazy Eager Loading
$posts = Post::all();
$posts->load('author');

Đúng vậy, việc sử dụng hàm with() hay load() ta có thể load 1 lúc ra tất cả tác giả thuộc tất cả các bài viết chỉ bằng 1 câu lệnh SQL. Bản chất của logic trên là chúng ta sẽ thực hiện 1 lệnh sql để lấy ra tất cả các Post, sau đó sẽ thực hiện thêm 1 câu sql để lấy tất cả Author có id nằm trong list author_id của danh sách post vừa lấy ra. Giống như thực hiện lệnh WHEREIN của sql mà thôi.

Vậy là tèn ten, thay vì mình phải thực hiện 1 loạt lệnh SQL để lấy thông tin tác giả thì hiện tai mình chỉ cần thực hiện 2 câu sql. Nghe có vẻ cao siêu nhưng thức tế lại là những thứ mà chúng ta đã được học ở môn cơ sở dữ liệu đúng không =))).

Nhưng khoan đã, nhìn đoạn code bên trên chúng ta thấy laravel hỗ trợ tận 2 hàm lận, vậy khi nào sử dụng with() và khi nào sử dụng load()?

Eager Loading : Sử dụng with() hay load()?

Sử dụng with() hay load()?

Điểm khác biệt duy nhất giữa with() và load() là hàm with() sẽ thực hiện ngay sau khi gặp các câu lệnh như get()first()all(), . . còn load() thì sẽ chạy sau khi load xong toàn bộ các bản ghi ra.

Nói 1 cách dễ hiểu thì dù ta có sử dụng with() hay load() thì truy vấn thực thi sẽ như này:

SELECT * FROM posts;
SELECT * FROM authors WHERE post_id IN (1, 2, 3, 4, 5, ...);

Tuy nhiên nó có 1 chút khác biệt về cách load mối quan hệ:

Nếu ta dùng:

$posts = Post::with('author')->get();

Có nghĩa là ngay sau khi chạy tới get() thì cả 2 câu lệnh bên trên sẽ được thực thi ngay.

Còn khi ta dùng:

$posts = Post::all()->load('author');

khi chạy tới all() thì chương trình sẽ chỉ run câu lệnh đầu tiên, sau khi gọi tới load(‘author‘) mới run câu lệnh thứ 2.

Bạn sẽ thấy sự khác nhau rõ ràng hơn nếu viết tách chúng ra thành 2

$posts = Post::all();
$posts = $posts->load('author');

Vậy khi nào nên sử dụng with() khi nào nên sử dụng load()?. Nếu bạn chắc chắn rằng mình cần chạy cả 2 câu lệnh trên thì hãy sử dụng with(). Còn trong trường hợp nào đó bạn chưa cần load quan hệ của chúng thì chỉ hãy sử dụng load().

Ví dụ: Màn hình post list của bạn có 1 option cho phép ta ẩn hiện thông tin tác giả, vậy trường hợp này ta có thể sử dụng load(). Hoặc trong trường hợp chỉ cần lấy thông tin của bài viết mà không cần thông tin của tác giả thì khi đó ta chỉ nên dùng load().

Khi nào nên sử dụng Eager Loading?

Khi nào nên sử dụng Eager Loading

Vậy câu hỏi đặt ra là khi nào nên sử dụng Eager Loading? Câu trả lời là hãy cố gắng dùng Eager Loading nếu có thể vì chúng sẽ giúp chúng ta loại bỏ vấn đề N+1 và tăng performance của bạn lên rất nhiều. Laravel không chỉ hỗ trợ load 1 quan hệ như trên mà Eager Loading còn có thể load 1 lúc nhiều quan hệ (Multiple Relationships Eager Loading), load quan hệ chồng nhau (Nested Eager Loading), hay thực hiện Eager Loading có kèm điều kiện nhất định …(Các bài viết chi tiết về các mối quan hệ này sẽ được update sau).

Thông tin về cách sử dụng Eager Loading bạn có thể xem tại trang chủ của laravel tại đây.

Tổng kết

Bài viết này mình đã giới thiệu về N+1 là gì, cách giải quyết bằng eager loading, sự khác nhau giữa phương phức load()with() của eager loading. Nếu thấy hay thì hãy chia sẻ cho bạn bè của mình nhé.

Ở bài viết tiếp theo mình sẽ đề cập tới vấn đề cũng rất quan trọng trong laravel mà ít người để ý tới khi code, Nếu anh em nào quan tâm có thể xem thêm tại đây nhé: Phân biệt Method và Properties trong laravel, 4 trường hợp không nên sử dụng Properties.


Bài viết liên quan

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *