Khắc phục hạn chế số lượng bản ghi khi truy vấn từ bảng quan hệ.
Đặt vấn đề Yêu cầu đặt ra là bạn có 2 model là Repo và Build, quan hệ giữa Repo và Build là quan hệ 1-n, giờ bạn muốn lấy ra tương ứng với mỗi bản ghi Repo n Build gần nhất một cách hiệu quả. Quan hệ Repo với Build được khai báo: public function builds ( ) { return $this - ...
Đặt vấn đề
Yêu cầu đặt ra là bạn có 2 model là Repo và Build, quan hệ giữa Repo và Build là quan hệ 1-n, giờ bạn muốn lấy ra tương ứng với mỗi bản ghi Repo n Build gần nhất một cách hiệu quả.
Quan hệ Repo với Build được khai báo:
public function builds() { return $this->hasMany(Build::class, 'repo_id', 'id'); }
Sau khi lướt qua documentation của Laravel, bạn sẽ thấy ngay func take() có thể là giải pháp cho vấn đề này,
To limit the number of results returned from the query, or to skip a given number of results in the query, you may use the skip and take methods.
Vậy thì bắt tay vào làm thôi:
Repo::with([ 'builds' => function ($q) { $q->orderByDesc('created_at')->take(3); }, ]) ->get();
Nhưng thật bất ngờ khi thử trong Tinker kết quả trả về như sau:
>>> AppModelsRepo::with([ 'builds' => function ($q) { $q->orderByDesc('created_at')->take(3); }, ]) ->get(); => IlluminateDatabaseEloquentCollection {#963 all: [ AppModelsRepo {#1552 id: 119, builds: IlluminateDatabaseEloquentCollection {#1228 all: [], }, }, AppModelsRepo {#1144 id: 144, builds: IlluminateDatabaseEloquentCollection {#1155 all: [ AppModelsBuild {#954 id: 4, repo_id: 144, number: 4 }, AppModelsBuild {#1156 id: 3, repo_id: 144, number: 3 }, AppModelsBuild {#1306 id: 2, repo_id: 144, number: 2 }, ], }, }, AppModelsRepo {#1154 id: 145, builds: IlluminateDatabaseEloquentCollection {#1147 all: [], }, }, AppModelsRepo {#1049 id: 146, builds: IlluminateDatabaseEloquentCollection {#1142 all: [], }, }, ], }
dù tất cả các Repo đều có nhiều hơn 3 Build, tuy nhiên kết quả trả về chỉ có 3 Build của 1 record duy nhất. Vọc vạch trên internet thì thấy cũng kha khá bạn vướng phải issue này, vậy solution cho nó là gì?
Giải pháp
Đầu tiên bạn tạo 1 class BaseModel chứa scope scopeNPerGroup
<?php namespace AppModels; use DB; use IlluminateDatabaseEloquentModel; class BaseModel extends Model { /** * query scope nPerGroup * * @return void */ public function scopeNPerGroup($query, $group, $n = 10) { // queried table $table = ($this->getTable()); // initialize MySQL variables inline $query->from(DB::raw("(SELECT @rank:=0, @group:=0) as vars, {$table}")); // if no columns already selected, let's select * if (!$query->getQuery()->columns) { $query->select("{$table}.*"); } // make sure column aliases are unique $groupAlias = 'group_' . md5(time()); $rankAlias = 'rank_' . md5(time()); // apply mysql variables $query->addSelect(DB::raw( "@rank := IF(@group = {$group}, @rank+1, 1) as {$rankAlias}, @group := {$group} as {$groupAlias}" )); // make sure first order clause is the group order $query->getQuery()->orders = (array) $query->getQuery()->orders; array_unshift($query->getQuery()->orders, ['column' => $group, 'direction' => 'asc']); // prepare subquery $subQuery = $query->toSql(); // prepare new main base QueryBuilder $newBase = $this->newQuery() ->from(DB::raw("({$subQuery}) as {$table}")) ->mergeBindings($query->getQuery()) ->where($rankAlias, '<=', $n) ->getQuery(); // replace underlying builder to get rid of previous clauses $query->setQuery($newBase); } }
Sau đó, bạn extends BaseModel ở cả 2 model Repo và Build.
Trong model Build bạn thêm quan hệ với Repo
public function repo() { return $this->belongsTo(Repo::class, 'repo_id', 'id'); }
Trong model Repo bạn thêm function như sau:
public function recentBuilds(int $limit = 3) { return $this->builds()->latest()->nPerGroup('repo_id', $limit); }
function trên sẽ lấy ra các Build gần gây nhất với số lượng $limit tùy chọn.
Vậy là xong, bạn chỉ cần gọi
Repo::with('recentBuilds')->get();
Kết quả nhận được sẽ bao gồm 3 Build tương ứng với mỗi Repo:
=> IlluminateDatabaseEloquentCollection {#1171 all: [ AppModelsRepo {#1066 id: 232, recentBuilds: IlluminateDatabaseEloquentCollection {#1053 all: [ AppModelsBuild {#1043 id: 263, repo_id: 232, number: 3 }, AppModelsBuild {#1074 id: 262, repo_id: 232, number: 2 }, AppModelsBuild {#1042 id: 261, repo_id: 232, number: 1 }, ], }, }, AppModelsRepo {#1049 id: 225, recentBuilds: IlluminateDatabaseEloquentCollection {#1070 all: [ AppModelsBuild {#1043 id: 246, repo_id: 225, number: 4 }, AppModelsBuild {#1074 id: 205, repo_id: 225, number: 3 }, AppModelsBuild {#1042 id: 204, repo_id: 225, number: 2 }, ], }, }, AppModelsRepo {#1067 id: 229, recentBuilds: IlluminateDatabaseEloquentCollection {#1058 all: [ AppModelsBuild {#1075 id: 245, repo_id: 229, number: 40 }, AppModelsBuild {#1041 id: 244, repo_id: 229, number: 39 }, AppModelsBuild {#1076 id: 243, repo_id: 229, number: 38 }, ], }, }, AppModelsRepo {#1050 id: 231, recentBuilds: IlluminateDatabaseEloquentCollection {#1178 all: [ AppModelsBuild {#1075 id: 201, repo_id: 231, number: 4 }, AppModelsBuild {#1041 id: 200, repo_id: 231, number: 3 }, AppModelsBuild {#1076 id: 199, repo_id: 231, number: 2 }, ], }, }, ], }
Kết luận
Bằng cách gọi tới scope NPerGroup, chúng ta đã khắc phục được nhược điểm của Laravel khi sử dụng Eager loading để lấy là n item quan hệ tương ứng với mỗi record.
Cảm ơn các bạn đã quan tâm, nếu có thắc mắc gì về cách cài đặt cứ comment nhé.