netsells / laravel-resourceful
Laravel API 资源增强,以消除 N+1 问题。
Requires
- php: ^8.1 || ^8.2
- ext-json: *
- laravel/framework: ^9.0 || ^10.0
Requires (Dev)
- netsells/code-standards-laravel: ^1.1
- orchestra/testbench: ^7.0 || ^8.0
- tmarsteel/mockery-callable-mock: ~2.1
This package is auto-updated.
Last update: 2024-09-03 09:53:34 UTC
README
帮助您使用 Laravel API 资源而不会杀死数据库的包!再见 N+1 👋
问题
想象一个具有“头像”关系的 User 模型
namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class UserResource extends JsonResource { public function toArray(): array { return [ 'id' => $this->id, 'name' => $this->name, 'avatar_url' => $this->avatar->url, ]; } }
为了从我们的 API 返回用户列表,我们可以有这样的控制器操作
namespace App\Http\Controllers; use App\Models\User; class UserController extends \Illuminate\Routing\Controller { public function index() { return UserResource::collection(User::paginate()); } }
在上面的例子中
- 如果数据库中有 1 个用户,将执行 2 个查询
- 如果数据库中有 100 个用户,将执行 101 个查询 - 1 个用于获取用户,然后为每个用户的每个头像执行 1 个查询
我们有一个经典的 N+1 问题。在 Laravel 中,通常的解决方案是在控制器中执行预加载
namespace App\Http\Controllers; use App\Models\User; use Illuminate\Routing\Controller; class UserController extends Controller { public function index() { return UserResource::collection(User::query()->with('avatar')->paginate()); } }
这很有效,但也有几个问题
- 控制器需要了解每个 API 资源的内结构
- 在上面的例子中,我们的控制器必须知道我们需要访问 User 资源中的 avatar 关系
- 这很难维护,因为我们需要保持多个地方同步
- 很难使预加载有条件
- 在上面的例子中,我们可能只想为某些会员计划的用户返回 avatar_url
- 如果我们不会返回 avatar_url,那么为什么一开始要加载 Avatar 模型呢?
- 很难保持预加载 DRY,因为它与控制器相关,而不是与 API 资源相关
- 想象一个 ArticleResource,它返回相关审稿人的列表,每个审稿人都是一个 UserResource
- 在控制器中获取文章需要预加载相关用户,以及每个用户的头像
- 由于预加载分散在多个地方,任何 N+1 优化都针对每个地方单独执行,并且只对该地方有益
- 其他依赖于受影响 API 资源的端点仍然存在 N+1 问题
此包解决了所有这些问题,并使 API 中的 N+1 问题成为过去式!
安装
使用 composer
composer require netsells/laravel-resourceful
用法
此包是官方 API 资源的替代品。我们不是扩展内置的 Illuminate\Http\Resources\Json\JsonResource
,而是扩展 Netsells\Http\Resources\Json\JsonResource
。
继续上面的例子,我们得到
namespace App\Http\Resources; use Netsells\Http\Resources\Json\JsonResource; class UserResource extends JsonResource { public function toArray(): array { return [ 'id' => $this->id, 'name' => $this->name, 'avatar_url' => $this->avatar->url, ]; } }
当然,这不会改变任何事情,事情仍然像有 N+1 问题一样工作,但是这个第一步允许你轻松切换到此包。
现在有几种方法可以增强您的 API 资源。
预加载方法
第一种方法是选择在专门的 preloads
方法中预加载的关系。你应该返回一个或多个“延迟资源”,这可以通过调用 preload
助手与您想要加载的关系列表轻松创建。preloads
方法可以可选地接受一个 Laravel 请求作为其第一个参数。在 preloads
方法中,我们有权访问正在解析的 Eloquent 模型。这允许我们根据请求参数/模型属性引入预加载特定关系的条件逻辑。
namespace App\Http\Resources; use Illuminate\Http\Request; use Netsells\Http\Resources\Json\JsonResource; class UserResource extends JsonResource { public function preloads() { return $this->preload('avatar'); } public function toArray(): array { return [ 'id' => $this->id, 'name' => $this->name, 'avatar_url' => $this->avatar->url, ]; } }
现在在调用 toArray
之前,将加载集合中所有用户的 avatar 关系,因此我们可以完全访问 $this->avatar
而不发出更多查询。
使用回调方法
第二种方法是提供一个回调,该回调将使用解析后的关系被调用。
namespace App\Http\Resources; use Illuminate\Http\Request; use Netsells\Http\Resources\Json\JsonResource; class UserResource extends JsonResource { public function toArray(): array { return [ 'id' => $this->id, 'name' => $this->name, 'avatar_url' => $this->use('avatar', fn ($avatar) => $avatar->url), ]; } }
现在当调用toArray
时,avatar
关系尚未加载。在toArray
方法中的我们的代码将针对集合中的所有用户执行,并且每次调用use
方法都会将一个关系加入加载队列。然后我们将以最优的方式解析所有队列中的关系,并将解析后的关系作为参数调用回调函数。
内联方法
第三种方法是使用内联快捷方式,这对于加载一个或多个嵌套API资源非常方便。
namespace App\Http\Resources; use Illuminate\Http\Request; use Netsells\Http\Resources\Json\JsonResource; class UserResource extends JsonResource { public function toArray(): array { return [ 'id' => $this->id, 'name' => $this->name, 'avatar' => $this->one('avatar', AvatarResource::class), ]; } } class AvatarResource extends JsonResource { public function preloads() { return $this->preload('file'); } public function toArray(): array { return [ 'id' => $this->id, 'width' => $this->width, 'height' => $this->height, 'url' => $this->file->s3Url(), ]; } }
在上面的例子中,我们介绍了一个嵌套资源,其中UserResource包含一个独立的AvatarResource。每个资源负责预加载它自己的关系,并且所有关系都以最优方式一起加载。