12/08/2018, 16:06
Laravel: Eloquent relationships
1. Vấn đề Input : Cho 2 bảng: Categories (id, name,..) và Posts (id, category_id, title, digest,..) có quan hệ 1-N. Output : Lấy ra danh sách categories, với mỗi category kèm theo 1 post mới nhất. 2. Thực hiện Quan hệ 1-N giữa 2 bảng được định nghĩa như sau: // Category model ...
1. Vấn đề
Input:
- Cho 2 bảng: Categories (id, name,..) và Posts (id, category_id, title, digest,..) có quan hệ 1-N.
Output:
- Lấy ra danh sách categories, với mỗi category kèm theo 1 post mới nhất.
2. Thực hiện
- Quan hệ 1-N giữa 2 bảng được định nghĩa như sau:
// Category model public function posts() { return $this->hasMany('Post'); } // Post model public function category() { return $this->belongsTo('Category'); }
- Để hiển thị danh sách các categories, có phân trang 20 category, với mỗi category hiển thị kèm theo 1 post mới nhất, ta cần làm như sau:
<h1>Categories</h1> @if ($categories->isEmpty()) <p>Sorry, no categories to show!</p> @else <ul> @foreach ($categories as $category) <li> <strong>{{ $category->name }}</strong> <span>latest post:</span> <article class="latest-post"> <div class="entry-title"> {{ $category->latestPost->title }} </div> <div class="entry-digest"> {{ $category->latestPost->digest }} </div> </div> </li> ... @endforeach </ul> @endif
- Đến đây ta cần tìm ra latestPost là bài toán sẽ được giải quyết. Bắt đầu nhé:
- Nếu ta chỉ có 1 category thì rất đơn giản đúng không:
$category = Category::first(); $latestPost = $category->posts()->latest()->first(); // select * from `posts` where `posts`.`category_id` = 1 order by `created_at` desc limit 1
- Việc lấy ra latestPost chỉ tương đương với 1 câu query bên dưới, nhưng đời nào lại chỉ có 1 category thôi, nếu là 20 category hoặc nhiều hơn nữa thì sao:
$categories = Category::take(20)->get(); // then in the view: @foreach ($categories as $category) {{ $category->posts()->latest()->first()->title }} @endforeach
- Kết quả thu được vẫn đúng, nhưng bạn có thấy điều gì bất ổn ở đây không, vòng lặp sẽ tạo tương ứng 20 query tương tự truy cập DB (facepalm)
- Giải pháp tránh tạo ra quá nhiều query là sử dụng Eager loading:
$categories = Category::with(['posts' => function ($q) { $q->latest(); // sorting related table, so we can use first on the collection }])->take(20)->get(); // in the view let's work with the collection instead of querying db @foreach ($categories as $category) {{ $category->posts->first()->title }} @endforeach
- Tuyệt vời, 1 query thay thế cho 20 query, có vẻ như đã xong phim, ngon lành rồi nhỉ?
- Nhưng để ý lại, ta chỉ cần với mỗi category kèm theo 1 post mới nhất, trong khi đó ta load ra rất nhiều posts không cần thiết rồi lại lấy first $category->posts->first() từ collection. Nếu mỗi category có hàng nghìn, vài chục nghìn posts thì hiệu suất không ổn chút nào.
- Ta sẽ thêm limit cho eager load xem sao:
$categories = Category::with(['posts' => function ($q) { $q->latest()->limit(1); }])->take(20)->get(); // select * from `posts` where `posts`.`category_id` in (1..20) order by `created_at` desc limit 1
- Việc thêm limit cho eager_load ta sẽ thu được câu query tương ứng bên trên. Và như vậy kết quả ta thu được là râu ông nọ cắm cằm bà kia, category này có thể sẽ đi kèm với post của category khác. Fail.. Hazz
- Như vậy gần như chắc chắn ta sẽ phải đánh đổi bộ nhớ ram để lưu hàng nghìn posts được load ra mà không dùng đến. Thế nhưng đó là đối với quan hệ 1-N, hasMany() đi với latest() thì đó là điều hiển nhiên. Bây giờ ta sẽ thử tạo ra 1 quan hệ mới để lấy ra latestPost:
// Category model public function latestPost() { return $this->hasOne('Post')->latest(); } public function posts() { return $this->hasMany('Post'); }
- Khi đó việc lấy latestPost sẽ như sau:
$categories = Category::with('latestPost')->take(20)->get(); // select * from `posts` where `posts`.`category_id` in (1..20) order by `created_at` desc // then: @foreach ($categories as $category) {{ $category->latestPost->title }} @endforeach
- Ngon rồi, 1 query lấy ra mỗi category kèm 1 post mới nhất, hiểu suất cũng ngon lành.