netsells/laravel-resourceful

Laravel API 资源增强,以消除 N+1 问题。

v1.2.0 2023-05-15 08:44 UTC

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。每个资源负责预加载它自己的关系,并且所有关系都以最优方式一起加载。

更多示例可以在测试中看到。