Quay lại

Query N +1 Trong Laravel Là Gì? Cách Giải Quyết Query N + 1 Chuyên mục PHP và Laravel    2024-05-22    13 Lượt xem    8 Lượt thích    comment-3 Created with Sketch Beta. 0 Bình luận

Query N +1 Trong Laravel Là Gì? Cách Giải Quyết Query N + 1

Trong Laravel và các hệ thống ORM khác, "query N + 1" là một vấn đề hiệu suất phổ biến xảy ra khi bạn tải các model chứa relationship trong một vòng lặp. Khi đó, thay vì thực hiện một truy vấn duy nhất để tải tất cả dữ liệu cần thiết THÌ mỗi lần lặp, một truy vấn riêng sẽ được thực hiện để tải dữ liệu từ bảng relationship liên quan.

Ví dụ về Query N + 1 trong Laravel

Giả sử bạn có hai bảng usersposts, với mỗi user có nhiều post. Bây giờ, bạn muốn hiển thị danh sách các users cùng với số lượng bài viết mà mỗi user đã đăng.

Model User và Post

//User.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}​
//Post.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    //
}​

Controller và View

// UserController.php
namespace App\Http\Controllers;

use App\Models\User;

class UserController extends Controller
{
    public function index()
    {
        $users = User::all();

        return view('users.index', compact('users'));
    }
}

// users/index.blade.php
@foreach ($users as $user)
    <p>{{ $user->name }} has {{ $user->posts->count() }} posts.</p>
@endforeach​

Vấn Đề: Query N + 1

Trong trường hợp này, nếu có 10 users và mỗi user đã đăng 5 post, khi bạn thực hiện lặp qua danh sách users để hiển thị số lượng bài viết của mỗi user, Laravel sẽ thực hiện 10 truy vấn để lấy số lượng bài viết của mỗi user. Điều này xảy ra vì mỗi lần lặp, Laravel sẽ thực hiện một truy vấn riêng để lấy số lượng bài viết của mỗi user, dẫn đến việc thực hiện nhiều truy vấn nhỏ hơn thay vì một truy vấn lớn.

Dưới đây là cách Laravel sẽ thực hiện các truy vấn trong trường hợp này:

  1. Truy vấn đầu tiên để lấy tất cả users:
SELECT * FROM `users`;​

Giả sử truy vấn này trả về 10 users:

$users = User::all();​
  1. Truy vấn riêng biệt cho mỗi user để lấy các posts của user đó:

Trong vòng lặp, Laravel sẽ thực hiện một truy vấn riêng cho mỗi user để lấy các posts của user đó:

foreach ($users as $user) {
    echo $user->name . ' has ' . $user->posts->count() . ' posts.' . PHP_EOL;
}​

Trong trường hợp này, $user->posts->count() sẽ thực hiện một truy vấn để đếm số lượng posts của mỗi user:

SELECT COUNT(*) FROM `posts` WHERE `user_id` = 1;
SELECT COUNT(*) FROM `posts` WHERE `user_id` = 2;
SELECT COUNT(*) FROM `posts` WHERE `user_id` = 3;
...
SELECT COUNT(*) FROM `posts` WHERE `user_id` = 10;​

Như vậy, nếu có 10 users, Laravel sẽ thực hiện 1 truy vấn để lấy tất cả users và thêm 10 truy vấn riêng biệt để lấy số lượng posts của mỗi user, tổng cộng là 11 truy vấn.

Giải Pháp

Để giải quyết vấn đề query N + 1, bạn có thể sử dụng phương thức with() để tải trước quan hệ một cách hiệu quả:

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

Khi sử dụng with(), Laravel sẽ chỉ thực hiện hai truy vấn: Một để lấy tất cả users và một để lấy tất cả posts, giúp tránh được vấn đề query N + 1.

Quy Trình Chi Tiết Cách with() Hoạt Động

  1. Truy Vấn Lấy Tất Cả Các Bản Ghi Chính:

Khi bạn gọi User::with('posts')->get(), truy vấn đầu tiên được thực hiện để lấy tất cả các bản ghi từ bảng users:

SELECT * FROM `users`;​

Giả sử bạn nhận được kết quả như sau:

$users = [
    (object) ['id' => 1, 'name' => 'Alice'],
    (object) ['id' => 2, 'name' => 'Bob'],
    (object) ['id' => 3, 'name' => 'Charlie'],
    ...
    (object) ['id' => 10, 'name' => 's2sontech'],
];​
  1. Truy Vấn Lấy Tất Cả Các Bản Ghi Liên Quan:

Laravel sau đó thực hiện một truy vấn thứ hai để lấy tất cả các bản ghi từ bảng postsuser_id nằm trong danh sách các id của các users đã được lấy ở truy vấn đầu tiên:

SELECT * FROM `posts` WHERE `user_id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

Giả sử bạn nhận được kết quả như sau:

$posts = [
    (object) ['id' => 1, 'user_id' => 1, 'title' => 'Post 1'],
    (object) ['id' => 2, 'user_id' => 1, 'title' => 'Post 2'],
    (object) ['id' => 3, 'user_id' => 2, 'title' => 'Post 3'],
    (object) ['id' => 4, 'user_id' => 3, 'title' => 'Post 4'],
    (object) ['id' => 5, 'user_id' => 3, 'title' => 'Post 5'],
];​
  1. Ánh Xạ Kết Quả:

Laravel sau đó ánh xạ các bản ghi posts vào các bản ghi users tương ứng trong bộ nhớ. Quá trình này tự động được thực hiện bởi Eloquent, sử dụng các mối quan hệ đã được định nghĩa trong các model.

Cách Laravel Thực Hiện Ánh Xạ

Laravel sử dụng các mối quan hệ đã được định nghĩa trong các model để ánh xạ các bản ghi. Ví dụ, nếu bạn đã định nghĩa quan hệ hasMany trong model User:

class User extends Model
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}​

Laravel sẽ sử dụng quan hệ này để ánh xạ các bản ghi posts vào các bản ghi users:

  • Khởi tạo một mảng trống posts trong mỗi đối tượng user.
    • // Khởi tạo mảng trống 'posts' cho mỗi user
      foreach ($users as $user) {
          $user->posts = [];
      }
      
      // Thêm các post vào mảng 'posts' của user tương ứng
      foreach ($posts as $post) {
          foreach ($users as $user) {
              if ($user->id == $post->user_id) {
                  $user->posts[] = $post;
              }
          }
      }
      
  • Duyệt qua từng post và thêm nó vào mảng posts của user tương ứng dựa trên user_id.

Laravel thực hiện điều này trong bộ nhớ mà không cần sự can thiệp thủ công từ phía bạn.

Ví Dụ Minh Họa

Sau khi ánh xạ, dữ liệu sẽ trông như thế này trong bộ nhớ:

$users = [
    (object) ['id' => 1, 'name' => 'Alice', 'posts' => [
        (object) ['id' => 1, 'user_id' => 1, 'title' => 'Post 1'],
        (object) ['id' => 2, 'user_id' => 1, 'title' => 'Post 2'],
    ]],
    (object) ['id' => 2, 'name' => 'Bob', 'posts' => [
        (object) ['id' => 3, 'user_id' => 2, 'title' => 'Post 3'],
    ]],
    (object) ['id' => 3, 'name' => 'Charlie', 'posts' => [
        (object) ['id' => 4, 'user_id' => 3, 'title' => 'Post 4'],
        (object) ['id' => 5, 'user_id' => 3, 'title' => 'Post 5'],
    ]],
];​

Bây giờ, khi bạn truy cập thuộc tính posts của mỗi user, bạn sẽ thấy rằng các post đã được tải trước và không cần thực hiện thêm truy vấn nào:

// users/index.blade.php
@foreach ($users as $user)
    <p>{{ $user->name }} has {{ $user->posts->count() }} posts.</p>
@endforeach​

Điều này cho phép bạn tránh được vấn đề N + 1 query và tối ưu hóa hiệu suất của ứng dụng một cách đáng kể. Laravel Eloquent thực hiện quá trình ánh xạ này tự động, giúp bạn làm việc với dữ liệu một cách hiệu quả và dễ dàng hơn.

Bình luận (0)

Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough
Michael Gough

Bài viết liên quan

Learning English Everyday