Eager loading get n related models per parent in Laravel
Đặt vấn đề Yêu cầu đặt ra khá đơn giản là mình có 1 bảng Post và 1 bảng Comment, 1 post có nhiều comments, bây giờ mình muốn lấy tất cả các bài post và mỗi bài post mình muốn lấy 1 comment mới nhất sử dụng Eager loading. Post::with([ 'comments' => function ($query) { ...
Đặt vấn đề
Yêu cầu đặt ra khá đơn giản là mình có 1 bảng Post và 1 bảng Comment, 1 post có nhiều comments, bây giờ mình muốn lấy tất cả các bài post và mỗi bài post mình muốn lấy 1 comment mới nhất sử dụng Eager loading.
Post::with([ 'comments' => function ($query) { $query->orderByDesc('created_at')->take(1); }, ]) ->get();
Nó query như này
select * from `posts` limit 1 select * from `posts` select * from `comments` where `comments`.`post_id` in ('1', '2', '3') order by `created_at` desc limit 1
Nhìn query trên thì chắc chắn bạn đã đoán ra kết quả như thế nào rồi đấy, nó chỉ lấy duy nhất 1 comment của một trong 3 bài post có id 1,2,3 mà có created_at mới nhất mà thôi, cách này không giải quyết được vấn đề rồi.
Giải pháp
Cách đơn giản nhất để giải quyết vấn đề trên là mình sẽ tạo thêm một relation hasOne trong model Post để lấy ra comment mới nhất của bài post đó như sau:
public function latestComment() { return $this->hasOne(Comment::class)->latest(); }
Mình chỉnh lại đoạn trên như sau:
Post::with('latestComment')->get();
Bây giờ thì ta sẽ lấy được những comments mới nhất của các bài post. Tuy nhiên nếu muốn lấy 2 hoặc 3 comments mới nhất của từng bài post thì phải làm thế nào, lúc này chắc chắn không thể sử dụng quan hệ hasOne ở trên rồi. Mình tạo class AbstractEloquent với scope scopeNPerItem như sau:
<?php namespace AppEloquent; use IlluminateDatabaseEloquentModel; use DB; abstract class AbstractEloquent extends Model { public function scopeNPerItem($query, $group, $n = 5) { $table = ($this->getTable()); $query->from(DB::raw("(SELECT @rank:=0, @group:=0) as vars, {$table}")); if ( ! $query->getQuery()->columns) { $query->select("{$table}.*"); } $groupAlias = 'group_' . md5(time()); $rankAlias = 'rank_' . md5(time()); $query->addSelect(DB::raw( "@rank := IF(@group = {$group}, @rank+1, 1) as {$rankAlias}, @group := {$group} as {$groupAlias}" )); $query->getQuery()->orders = (array) $query->getQuery()->orders; array_unshift($query->getQuery()->orders, [ 'column' => $group, 'direction' => 'asc', ]); $subQuery = $query->toSql(); $newBase = $this->newQuery() ->from(DB::raw("({$subQuery}) as {$table}")) ->mergeBindings($query->getQuery()) ->where($rankAlias, '<=', $n) ->getQuery(); $query->setQuery($newBase); } }
Bây giờ model Post mình sẽ extends AbstractEloquent thay vì Model. Mình thêm một function trong model Post để lấy ra 2 comments mới nhất như sau:
public function takeComments() { return $this->comments()->nPerItem('post_id', 2)->latest(); }
Bây giờ muốn lấy 2 comments mới nhất cho mỗi bài post sử dụng eager loading sẽ như sau:
Post::with('takeComments')->get();
Kết luận
Hi vọng bài viết sẽ giúp ích gì đó cho bạn. Cảm ơn.