Bạn đã sử dụng đúng API Resources trong Laravel - N + 1 đây

2nd Nov 2022
Bạn đã sử dụng đúng API Resources trong Laravel - N + 1 đây
Table of contents

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 !

Bạn thấy bài viết này như thế nào?
0 reactions

Add new comment

Image CAPTCHA
Enter the characters shown in the image.

Related Articles

Danh sách này có 4 sản phẩm nên chưa cần phân trang, nếu số lượng sản phẩm lên đến vài chục sản phẩm

Hiện nay kiến trúc Microservices đang là chủ đề được cộng đồng Developer vô cùng quan tâm

Hôm nay chúng ta cùng tìm hiểu về Eloquent trong Laravel với mối quan hệ nhiều - nhiều (many to many relationship)

Kiểm soát hợp lí truy cập nội dung trang web là yếu tố quyết định trong việc điều hành một máy chủ bảo mật.

Trong lập trình web, authorization (phân quyền) là chức năng vô cùng quan trọng và không thể thiếu