Bạn đã sử dụng đúng API Resources trong Laravel ?
Trong Laravel, API Resources giúp bạn cần chuyển đổi dữ liệu Eloquent model và JSON được trả về bởi ứng dụng của người dùng ( thêm, thay đổi giá trị hay loại bỏ thuộc tính của model ) khi dựng một API.
Phạm vi bài viết:
Tập trung làm rõ : " Các lỗi thường gặp phải và cách tối ưu code " ~ cũng là vấn đề mình gặp phải trong quá trình làm dự án công ty. Bởi vậy, trong bài viết mình xin loại bỏ các bước chuẩn bị file và dữ liệu nhé
Đơn giản, mình sẽ một loạt các yêu cầu bài toán với đầu vào dữ liệu được lấy từ 2 bảng Songs (id, title, artist, rating, album_id ) và Album (name, user) ( n-1 ).
1. Lỗi thường gặp
Lẫn lộn giữa Resouce và ResouceCollection
Yêu cầu bài toán: mong muốn lấy ra danh sách bài hát thông qua API và các thông tin tương ứng cần có
# routes/api.php
Route::get('/songs', function() {
return new SongResource(Song::all());
});
class SongsResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'artist' => $this->artist
];
}
}
Kết quả: Lỗi thông báo Property [id] does not exist on this collection instance.
xuất hiện vì SongsResource
chỉ cho phép nhận một giá trị đơn lẻ chứ không phải một tập giá trị collection =)).
Vậy đến đây bạn sẽ giải quyết như thế nào ?
Mình có đưa ra 2 giải pháp tùy thuộc vào dữ liệu mong muốn cần hiện thị
- Chỉ hiện có các trường đã được định nghĩa trong
SongResource
:
Đơn giản hãy chuyển !
# routes/api.php
// Lấy danh sách các bài hát
Route::get('/songs', function() {
return SongsResource::collection(Songs::all());
});
- Thêm trường mới vào dữ liệu trả về
Hãy sử dụng ResouceCollection để giải quyết nó !.
class SongsCollection extends ResourceCollection
{
public function toArray($request)
{
return [
'data' => SongsResource::collection($this->collection),
'meta' => [
'count' => $this->collection->count(),
'link' => 'value-link',
],
];
}
}
Route::get('/songs', function() {
return new SongsCollection(Songs::all());
});
Kết quả: trường thêm mới count
và link
được thêm mới vào và với việc sử dụng SongsResource::collection($this->collection)
bạn có thể tùy chỉnh các trường dữ liệu trả về từ SongsResource
.
2. Tối ưu code
- Giải quyết vấn đề n+1 query
Yêu cầu bài toán: thêm 1 trường mới album
vào SongsResource
để miêu tả thêm thông tin bài hát
class SongsResource extends JsonResource
{
public function toArray($request)
{
return [
// new field which don't belongs to songs table
'album' => new AlbumResource($this->album),
// List of fields in songs table
'title' => $this->title,
'artist' => $this->artist
];
}
}
Cách giải quyết thông thường:
Route::get('/songs', function() {
return new SongsCollection(Songs::all());
});
class SongsCollection extends ResourceCollection
{
public function toArray($request)
{
return [
'data' => SongsResource::collection($this->collection),
'meta' => [
'count' => $this->collection->count(),
],
];
}
}
class AlbumResource extends JsonResource
{
public function toArray($request)
{
return [
'name' => $this->name,
];
}
}
Kết quả: N + 1 query xuất hiện
string(21) "select * from `songs`"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
string(52) "select * from `album` where `album`.`id` = ? limit 1"
{"data":[{"album":{"name":"Pray how."},"title":"Cat, 'if.","artist":"Cheshire."}],"meta":{"count":20}}
Chú ý: thêm đoạn mã nào vào routes\api.php
để debug
\DB::listen(function($query) {
var_dump($query->sql);
});
Vậy đến đây bạn sẽ giải quyết bằng cách nào ?
Với mình thì chỉ cần chỉ cần thêm Songs::with('album')->get()
là xong ~
Route::get('/songs', function() {
return new SongsCollection(Songs::with('album')->get());
});
Kết quả:
string(21) "select * from `songs`"
string(75) "select * from `album` where `album`.`id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)"
{"data":[{"album":{"name":"Pray how."},"title":"Cat, 'if.","artist":"Cheshire."}],"meta":{"count":20}}
- Code ngắn gọn
Yêu cầu bài toán: Thêm 2 trường mới songs
và updated_at
miêu tả vào rõ hơn về ngày tạo album và danh sách bài hát trong đó AlbumResource
class AlbumResource extends JsonResource
{
public function toArray($request)
{
return [
'name' => $this->name
}
}
class SongsResource extends JsonResource
{
public function toArray($request)
{
return [
'title' => $this->title,
'artist' => $this->artist
];
}
}
Route::get('/songs', function() {
return new SongsCollection(Songs::with('album')->get());
});
Cách giải quyết thông thường:
class AlbumResource extends JsonResource
{
public function toArray($request)
{
$songs = collect($this->songs)->map(function ($item){
return [$item->title,$item->artist];
});
$result = [
'name' => $this->name,
// other attributes
'songs' => $songs,
];
if($this->songs->count() > 3) {
$result['updated_at'] = '02082021';
}
return $result;
}
}
Hãy sử dụng whenLoaded
và mergeWhen
để tối ưu code trong trường hợp này -> loại bỏ bớt if else và logic xử lý ngoài lề.
Chú ý: whenLoaded
phải đi kèm với đầu vào là Songs::with('album')->get()
là một Eager Loading
.
class AlbumResource extends JsonResource
{
public function toArray($request)
{
return [
'name' => $this->name,
// other attributes
'songs' => SongsResource::collection($this->whenLoaded('songs')),
$this->mergeWhen($this->songs->count() > 3
, ['updated_at' => '02082021'])
];
}
}
Rất mong bài viết có thể giúp ích cho mọi người và nhận được sự đóng góp từ mọi người !