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. Lấy một đối tượng từ quan hệ one-many, ví dụ ta có 2 đối tượng là Post
và Comment
như sau đây:
Giả sử một ngày đẹp trời, bạn sẽ cần lấy ra một danh sách bài viết và các 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àu, 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 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ết và bì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.
Tham khảo: