[Laravel] Giải quyết vấn đề bộ nhớ khi sử dụng Eager Loading

Hình ảnh minh hoạ

Vấn đề

Hôm nay chúng ta sẽ đưa đến một vấn đề không phải mới, và chắc là các bạn cũng đã từng giải quyết rồi, đó chính là lấy một đối tượng từ quan hệ hasMany, ví dụ ta có 2 đối tượng là PostComment như sau đây:

Giả sử bạn sẽ cần lấy ra một danh sách bài viết và một bình luận mới nhất từng bài viết đó, bạn sẽ làm thế nào?

Chúng ta có rất nhiều cách để thực hiện đề bài này

  1. Thực hiện bình thường bằng Laravel relationship
  2. Thực hiện bằng Laravel relationship & Eager loading
  3. Thực hiện bằng Dynamic relationship & Eager loading

Hãy cùng nhau tìm hiểu qua từng cách làm cũng như ưu và khuyết điểm của mỗi cách tiếp cận nhé

Sử dụng Laravel relationship

Trong Laravel bạn dễ dàng thực hiện được việc này thông qua model Post và quan hệ comments, mình bỏ qua bước tạo project nhé, ta đi tiếp vào ví dụ dưới đây:

// Trong class Post.php ta có:
public function comments()
{
  return $this->hasMany('App\Models\Comment');
}

Và trong controller ta chỉ cần gọi all() và truyền dữ liệu qua view:

$posts = Post::all();
return view('list', compact('posts'));

Trong view sẽ có 2 cột, tên bài viếtbình luận mới nhất:

<table>
    <thead>
    <tr>
        <th>Bài viết</th>
        <th>Bình luận</th>
    </tr>
    </thead>
    <tbody>
    @foreach($posts as $post)
        <tr>
            <td>{{ $post->title }}</td>
            <td>{{ $post->comments->sortByDesc('created_at')->first()->content }}</td>
        </tr>
    @endforeach
    </tbody>
</table>

Kết quả:

12 lần truy vấn CSDL trong một lần, đối với ví dụ này, số lượng không phải là quá nhiều, tuy nhiên với một số lượng lớp DB đến vài ngàn bài viết thì có vẻ sẽ rất tệ.

Sử dụng relationship và Eager loading

Như đã nói ở bài viết trước, vấn đề truy vấn của Laravel có thể dễ dàng giải quyết bằng Eager loading. Ta sẽ tạo các relationship trong các model Post như sau:

// HasMany
public function comments()
{
  return $this->hasMany('App\Models\Comment');
}

Controller:

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

View:

<table>
    <thead>
    <tr>
        <th>Bài viết</th>
        <th>Bình luận</th>
    </tr>
    </thead>
    <tbody>
    @foreach($posts as $post)
        <tr>
            <td>{{ $post->title }}</td>
            <td>{{ $post->comments->sortByDesc('created_at')->first()->content }}</td>
        </tr>
    @endforeach
    </tbody>
</table>

Kết quả:

Vẫn là Eager Loading, nhưng là hasOne

Như bạn thấy, chúng ta đã giảm lượng truy vấn xuống còn 2 truy vấn, bạn cũng có thể tối ưu cho code đẹp hơn bằng cách tạo quan hệ hasOne giữa 2 đối tượng.

// HasOne
public function latest_comment()
{
  return $this->hasOne('App\Models\Comment')->latest();
}

Controller:

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

View:

<table>
    <thead>
    <tr>
        <th>Bài viết</th>
        <th>Bình luận</th>
    </tr>
    </thead>
    <tbody>
    @foreach($posts as $post)
        <tr>
            <td>{{ $post->title }}</td>
            <td>{{ $post->latest_comment->content }}</td>
        </tr>
    @endforeach
    </tbody>
</table>

Kết quả:

Hola! code đã đẹp và rất dễ đọc. Tuy nhiên (lại tuy nhiên) nếu bạn để ý thấy ta chỉ cần dùng 20 model (10 post và 10 comment mới nhất) nhưng ở đây lại load đến 10010 model, tức là nó sẽ lấy ra 10 bài viết và tất cả bình luận của 10 bài viết đó 😱😱 Nếu bạn có một máy chủ không giới hạn dung lượng, việc này không sao, tuy nhiên nó sẽ làm giảm đáng kể khả năng xử lý và có vẻ không ổn, Hãy luôn ghi nhớ:

Database queries first, memory usage second

Giải quyết bằng Dynamic relationship

Trong ví dụ trên, ta đã thành công trong việc giảm thiểu tối đa các truy vấn không cần thiết nhưng vô tình đã làm tăng dung lượng ram. Hãy luôn nhớ "Database queries first, memory usage second" Việc này có thể giải quyết bằng cách thực hiện một Subquery Select và tạo một relationship belongsTo cho Post

Nhìn vào hình ở trên cho dễ hiểu, khi thực hiện truy vấn, ta sẽ thêm vào một cột tên là latest_comment_id, cột này được lấy từ bảng comments với các điều kiện đặt trước.

// Relationship
public function latest_comment()
{
    return $this->belongsTo('App\Models\Comment', 'latest_comment_id', 'id');
}
// Subquery
public function scopeWithLatestComment($query)
{
    $query->addSelect([
        'latest_comment_id' => Comment::select('id')
            ->whereColumn('post_id', 'posts.id')
            ->orderBy('created_at', 'desc')
            ->take(1)
    ])->with('latest_comment');
}

Controller:

$posts = Post::withLatestComment()->get();

View:

<table>
    <thead>
    <tr>
        <th>Bài viết</th>
        <th>Bình luận</th>
    </tr>
    </thead>
    <tbody>
    @foreach($posts as $post)
        <tr>
            <td>{{ $post->title }}</td>
            <td>{{ $post->latest_comment->content }}</td>
        </tr>
    @endforeach
    </tbody>
</table>

Kết quả:

Bingo! kết quả chỉ có 2 truy vấn, và 20 model được tải lên ứng dụng, bộ nhớ sử dụng đã giảm từ 33mb ~ 18mb. Vậy là vừa đảm bảo được 2 tiêu chí đặt ra. Nếu thấy bài viết này hữu ít đừng ngại chia sẻ cho bạn bè mình nhé. Cảm ơn bạn đã đọc ♥

Tham khảo:


All Rights Reserved