首页 >后端开发 >php教程 >Laravel模型提示

Laravel模型提示

百草
百草原创
2025-03-05 16:44:11390浏览

Laravel Model Tips

Laravel 提供了大量强大的功能,有助于提升我们的开发体验 (DX)。但是,随着定期发布、日常工作的压力以及大量可用功能的出现,很容易错过一些鲜为人知的功能,而这些功能可以帮助改进我们的代码。

本文将介绍一些我最喜欢的 Laravel 模型使用技巧。希望这些技巧能帮助你编写更简洁、更高效的代码,并帮助你避免常见的陷阱。

发现并防止 N 1 问题

我们将首先介绍如何发现并防止 N 1 查询问题。

当延迟加载关联关系时,可能会出现常见的 N 1 查询问题,其中 N 是运行以获取相关模型的查询次数。

这是什么意思呢?让我们来看一个例子。假设我们要从数据库中获取所有帖子,遍历它们,并访问创建帖子的用户。我们的代码可能如下所示:

$posts = Post::all();

foreach ($posts as $post) {
    // 对帖子执行某些操作...

    // 尝试访问帖子的用户
    echo $post->user->name;
}

尽管上面的代码看起来不错,但它实际上会导致 N 1 问题。假设数据库中有 100 个帖子。在第一行,我们将运行单个查询以获取所有帖子。然后,在访问 $post->userforeach 循环中,这将触发一个新查询以获取该帖子的用户;导致额外 100 个查询。这意味着我们将总共运行 101 个查询。正如你所想象的那样,这并不好!它会减慢应用程序的速度,并给数据库带来不必要的压力。

随着代码变得越来越复杂,功能越来越多,除非你积极寻找这些问题,否则很难发现这些问题。

值得庆幸的是,Laravel 提供了一个方便的 Model::preventLazyLoading() 方法,你可以使用它来帮助发现和防止这些 N 1 问题。此方法将指示 Laravel 在延迟加载关系时抛出异常,因此你可以确保始终热切加载你的关系。

要使用此方法,可以将 Model::preventLazyLoading() 方法调用添加到你的 AppProvidersAppServiceProvider 类中:

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Model::preventLazyLoading();
    }
}

现在,如果我们要运行上面的代码来获取每个帖子并访问创建该帖子的用户,我们将看到抛出 IlluminateDatabaseLazyLoadingViolationException 异常,并显示以下消息:

<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>

要解决此问题,我们可以更新代码,在获取帖子时热切加载用户关系。我们可以使用 with 方法来实现:

$posts = Post::with('user')->get();

foreach ($posts as $post) {
    // 对帖子执行某些操作...

    // 尝试访问帖子的用户
    echo $post->user->name;
}

上面的代码现在将成功运行,并且只会触发两个查询:一个用于获取所有帖子,另一个用于获取这些帖子的所有用户。

防止访问缺失的属性

你尝试访问你认为存在于模型上但不存在的字段的频率有多高?你可能输入错误了,或者你可能认为存在 full_name 字段,而实际上它被称为 name

假设我们有一个 AppModelsUser 模型,具有以下字段:

  • id
  • name
  • email
  • password
  • created_at
  • updated_at

如果我们运行以下代码会发生什么?:

$posts = Post::all();

foreach ($posts as $post) {
    // 对帖子执行某些操作...

    // 尝试访问帖子的用户
    echo $post->user->name;
}

假设我们在模型上没有 full_name 访问器,则 $name 变量将为 null。但我们不知道这是因为 full_name 字段实际上为 null,还是因为我们没有从数据库中获取该字段,或者因为该字段不存在于模型中。正如你所想象的那样,这可能会导致意想不到的行为,有时很难发现。

Laravel 提供了一个 Model::preventAccessingMissingAttributes() 方法,你可以使用它来帮助防止此问题。此方法将在你尝试访问模型当前实例上不存在的字段时指示 Laravel 抛出异常。

要启用此功能,可以将 Model::preventAccessingMissingAttributes() 方法调用添加到你的 AppProvidersAppServiceProvider 类中:

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Model::preventLazyLoading();
    }
}

现在,如果我们要运行我们的示例代码并尝试访问 AppModelsUser 模型上的 full_name 字段,我们将看到抛出 IlluminateDatabaseEloquentMissingAttributeException 异常,并显示以下消息:

<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>

使用 preventAccessingMissingAttributes 的另一个好处是,它可以突出显示我们尝试读取模型上存在的但可能未加载的字段的情况。例如,假设我们有以下代码:

$posts = Post::with('user')->get();

foreach ($posts as $post) {
    // 对帖子执行某些操作...

    // 尝试访问帖子的用户
    echo $post->user->name;
}

如果我们阻止访问缺失的属性,则会抛出以下异常:

$user = User::query()->first();

$name = $user->full_name;

这在更新现有查询时非常有用。例如,过去,你可能只需要模型中的几个字段。但是,你可能现在正在更新应用程序中的功能,并且需要访问另一个字段。如果没有启用此方法,你可能不会意识到你正在尝试访问尚未加载的字段。

值得注意的是,preventAccessingMissingAttributes 方法已从 Laravel 文档中删除 (commit),但它仍然有效。我不确定删除它的原因,但这是一个需要注意的问题。这可能表明它将来会被删除。

(以下内容与原文相同,为了保持一致性,我将保留原文,不再进行改写)

防止静默丢弃属性

preventAccessingMissingAttributes 类似,Laravel 提供了一个 preventSilentlyDiscardingAttributes 方法,可以帮助防止更新模型时出现意外行为。

假设你有一个 AppModelsUser 模型类,如下所示:

$posts = Post::all();

foreach ($posts as $post) {
    // 对帖子执行某些操作...

    // 尝试访问帖子的用户
    echo $post->user->name;
}

正如我们所看到的,nameemailpassword 字段都是可填充字段。但是,如果我们尝试更新模型上不存在的字段(例如 full_name)或存在的但不可填充的字段(例如 email_verified_at)会发生什么?:

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Model::preventLazyLoading();
    }
}

如果我们运行上面的代码,full_nameemail_verified_at 字段都将被忽略,因为它们没有被定义为可填充字段。但不会抛出错误,因此我们将不知道这些字段已被静默丢弃。

正如你所预期的那样,这可能会导致应用程序中难以发现的错误,特别是如果你的“更新”语句中的任何其他内容实际上都已更新。因此,我们可以使用 preventSilentlyDiscardingAttributes 方法,该方法将在你尝试更新模型上不存在或不可填充的字段时抛出异常。

要使用此方法,可以将 Model::preventSilentlyDiscardingAttributes() 方法调用添加到你的 AppProvidersAppServiceProvider 类中:

<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>

上面的代码将强制抛出错误。

现在,如果我们尝试运行上面的示例代码并更新用户的 first_nameemail_verified_at 字段,则会抛出 IlluminateDatabaseEloquentMassAssignmentException 异常,并显示以下消息:

$posts = Post::with('user')->get();

foreach ($posts as $post) {
    // 对帖子执行某些操作...

    // 尝试访问帖子的用户
    echo $post->user->name;
}

值得注意的是,preventSilentlyDiscardingAttributes 方法仅在你使用 fillupdate 等方法时才会突出显示不可填充字段。如果你手动设置每个属性,它将不会捕获这些错误。例如,让我们来看以下代码:

$user = User::query()->first();

$name = $user->full_name;

在上面的代码中,full_name 字段不存在于数据库中,因此 Laravel 不会为我们捕获它,而是在数据库级别捕获它。如果你使用的是 MySQL 数据库,你会看到这样的错误:

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Model::preventAccessingMissingAttributes();
    }
}

启用模型的严格模式

如果你想使用我们前面提到的三种方法,你可以使用 Model::shouldBeStrict() 方法一次启用它们。此方法将启用 preventLazyLoadingpreventAccessingMissingAttributespreventSilentlyDiscardingAttributes 设置。

要使用此方法,可以将 Model::shouldBeStrict() 方法调用添加到你的 AppProvidersAppServiceProvider 类中:

<code>属性 [full_name] 不存在或未为模型 [App\Models\User] 获取。</code>

这等同于:

$posts = Post::all();

foreach ($posts as $post) {
    // 对帖子执行某些操作...

    // 尝试访问帖子的用户
    echo $post->user->name;
}

preventAccessingMissingAttributes 方法类似,shouldBeStrict 方法已从 Laravel 文档中删除 (commit),但仍然有效。这可能表明它将来会被删除。

使用 UUID

默认情况下,Laravel 模型使用自动递增的 ID 作为其主键。但有时你可能更愿意使用通用唯一标识符 (UUID)。

UUID 是 128 位(或 36 个字符)的字母数字字符串,可用于唯一标识资源。由于它们是如何生成的,因此它们与另一个 UUID 冲突的可能性极低。一个 UUID 示例是:1fa24c18-39fd-4ff2-8f23-74ccd08462b0

你可能希望将 UUID 用作模型的主键。或者,你可能希望保留自动递增的 ID 来定义应用程序和数据库中的关系,但将 UUID 用于面向公众的 ID。使用这种方法可以通过使攻击者更难以猜测其他资源的 ID 来增加额外的安全层。

例如,假设我们在路由中使用自动递增的 ID。我们可能有一个用于访问用户的路由,如下所示:

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Model::preventLazyLoading();
    }
}

如果路由不安全,攻击者可以循环遍历 ID(例如 - /users/1/users/2/users/3 等),试图访问其他用户的资料。而如果我们使用 UUID,则 URL 可能更像 /users/1fa24c18-39fd-4ff2-8f23-74ccd08462b0/users/b807d48d-0d01-47ae-8bbc-59b2acea6ed3/users/ec1dde93-c67a-4f14-8464-c0d29c95425f。正如你所想象的那样,这些更难以猜测。

当然,仅仅使用 UUID 并不能保护你的应用程序,它们只是你可以采取的提高安全性的额外步骤。你需要确保你还使用其他安全措施,例如速率限制、身份验证和授权检查。

将 UUID 用作主键

我们首先来看一下如何将主键更改为 UUID。

为此,我们需要确保我们的表有一个能够存储 UUID 的列。Laravel 提供了一个方便的 $table->uuid 方法,我们可以在迁移中使用它。

假设我们有这个创建 comments 表的基本迁移:

<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>

正如我们在迁移中看到的,我们定义了一个 UUID 字段。默认情况下,此字段将被称为 uuid,但如果你愿意,可以通过向 uuid 方法传递列名来更改它。

然后,我们需要指示 Laravel 将新的 uuid 字段用作我们的 AppModelsComment 模型的主键。我们还需要添加一个特性,它将允许 Laravel 为我们自动生成 UUID。我们可以通过覆盖模型上的 $primaryKey 属性并使用 IlluminateDatabaseEloquentConcernsHasUuids 特性来实现:

$posts = Post::with('user')->get();

foreach ($posts as $post) {
    // 对帖子执行某些操作...

    // 尝试访问帖子的用户
    echo $post->user->name;
}

现在应该配置好模型并准备使用 UUID 作为主键了。来看这个示例代码:

$posts = Post::all();

foreach ($posts as $post) {
    // 对帖子执行某些操作...

    // 尝试访问帖子的用户
    echo $post->user->name;
}

我们可以在转储的模型中看到 uuid 字段已填充了 UUID。

添加 UUID 字段到模型

如果你更愿意将自动递增的 ID 用于内部关系,但将 UUID 用于面向公众的 ID,则可以向模型添加 UUID 字段。

我们假设你的表具有 iduuid 字段。由于我们将使用 id 字段作为主键,因此我们不需要在模型上定义 $primaryKey 属性。

我们可以覆盖通过 IlluminateDatabaseEloquentConcernsHasUuids 特性提供的 uniqueIds 方法。此方法应返回应为其生成 UUID 的字段数组。

让我们更新我们的 AppModelsComment 模型以包含我们称为 uuid 的字段:

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Model::preventLazyLoading();
    }
}

现在,如果我们要转储一个新的 AppModelsComment 模型,我们将看到 uuid 字段已填充了 UUID:

<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>

稍后我们将在本文中介绍如何更新你的模型和路由,以便在你的路由中使用这些 UUID 作为你的面向公众的 ID。

使用 ULID

与在 Laravel 模型中使用 UUID 类似,有时你可能希望使用通用唯一词典排序标识符 (ULID)。

ULID 是 128 位(或 26 个字符)的字母数字字符串,可用于唯一标识资源。一个 ULID 示例是:01J4HEAEYYVH4N2AKZ8Y1736GD

你可以像定义 UUID 字段一样定义 ULID 字段。唯一的区别是,你应该使用 IlluminateDatabaseEloquentConcernsHasUlids 特性,而不是更新你的模型以使用 IlluminateDatabaseEloquentConcernsHasUuids 特性。

例如,如果我们想更新我们的 AppModelsComment 模型以使用 ULID 作为主键,我们可以这样做:

$posts = Post::with('user')->get();

foreach ($posts as $post) {
    // 对帖子执行某些操作...

    // 尝试访问帖子的用户
    echo $post->user->name;
}

更改用于路由模型绑定的字段

你可能已经知道什么是路由模型绑定。但以防万一你不知道,让我们快速回顾一下。

路由模型绑定允许你根据传递到 Laravel 应用程序路由的数据自动获取模型实例。

默认情况下,Laravel 将使用模型的主键字段(通常是 id 字段)进行路由模型绑定。例如,你可能有一个用于显示单个用户信息的路由:

$user = User::query()->first();

$name = $user->full_name;

上面示例中定义的路由将尝试查找数据库中存在且具有提供的 ID 的用户。例如,假设数据库中存在 ID 为 1 的用户。当你访问 URL /users/1 时,Laravel 将自动从数据库中获取 ID 为 1 的用户,并将其传递给闭包函数(或控制器)以进行操作。但是,如果数据库中不存在具有提供的 ID 的模型,Laravel 将自动返回 404 Not Found 响应。

但是,有时你可能希望使用不同的字段(而不是主键)来定义如何从数据库中检索模型。

例如,正如我们前面提到的,你可能希望将自动递增的 ID 用作模型的主键用于内部关系。但你可能希望将 UUID 用于面向公众的 ID。在这种情况下,你可能希望使用 uuid 字段进行路由模型绑定,而不是 id 字段。

同样,如果你正在构建博客,你可能希望根据 slug 字段而不是 id 字段来获取你的帖子。这是因为 slug 字段比自动递增的 ID 更易于阅读且更利于 SEO。

更改所有路由的字段

如果你想定义应用于所有路由的字段,则可以通过在模型上定义 getRouteKeyName 方法来实现。此方法应返回你希望用于路由模型绑定的字段的名称。

例如,假设我们要将 AppModelsPost 模型的所有路由模型绑定更改为使用 slug 字段而不是 id 字段。我们可以通过向我们的 Post 模型添加 getRouteKeyName 方法来实现:

$posts = Post::all();

foreach ($posts as $post) {
    // 对帖子执行某些操作...

    // 尝试访问帖子的用户
    echo $post->user->name;
}

这意味着我们现在可以像这样定义我们的路由:

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Model::preventLazyLoading();
    }
}

当我们访问 URL /posts/my-first-post 时,Laravel 将自动从数据库中获取 slugmy-first-post 的帖子,并将其传递给闭包函数(或控制器)以进行操作。

更改单个路由的字段

但是,有时你可能只想更改单个路由中使用的字段。例如,你可能希望在一个路由中使用 slug 字段进行路由模型绑定,但在所有其他路由中使用 id 字段。

我们可以通过在路由定义中使用 :field 语法来实现。例如,假设我们要在一个路由中使用 slug 字段进行路由模型绑定。我们可以像这样定义我们的路由:

<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>

这现在意味着在这个特定路由中,Laravel 将尝试从数据库中获取具有提供的 slug 字段的帖子。

使用自定义模型集合

当你使用 AppModelsUser::all() 等方法从数据库中获取多个模型时,Laravel 通常会将它们放入 IlluminateDatabaseEloquentCollection 类的实例中。此类提供了许多用于处理返回的模型的有用方法。但是,有时你可能希望返回自定义集合类而不是默认集合类。

你可能出于几个原因想要创建一个自定义集合。例如,你可能想要添加一些特定于处理该类型模型的辅助方法。或者,你可能希望将其用于改进类型安全,并确保集合只包含特定类型的模型。

Laravel 使覆盖应返回的集合类型变得非常容易。

让我们来看一个例子。假设我们有一个 AppModelsPost 模型,当我们从数据库中获取它们时,我们希望将它们返回到自定义 AppCollectionsPostCollection 类的实例中。

我们可以创建一个新的 app/Collections/PostCollection.php 文件并像这样定义我们的自定义集合类:

$posts = Post::all();

foreach ($posts as $post) {
    // 对帖子执行某些操作...

    // 尝试访问帖子的用户
    echo $post->user->name;
}

在上面的示例中,我们创建了一个新的 AppCollectionsPostCollection 类,它扩展了 Laravel 的 IlluminateSupportCollection 类。我们还指定了此集合将只包含 AppModelsPost 类的实例,使用 docblock。这对于帮助你的 IDE 理解集合中将包含的数据类型非常有用。

然后,我们可以更新我们的 AppModelsPost 模型以返回自定义集合类的实例,方法是覆盖 newCollection 方法,如下所示:

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Model::preventLazyLoading();
    }
}

在这个例子中,我们获取传递给 newCollection 方法的 AppModelsPost 模型数组,并返回自定义 AppCollectionsPostCollection 类的新的实例。

现在我们可以使用自定义集合类从数据库中获取我们的帖子,如下所示:

<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>

比较模型

我在处理项目时遇到的一个常见问题是如何比较模型。这通常是在授权检查中,当你想检查用户是否可以访问资源时。

让我们来看一些常见的陷阱,以及为什么你可能应该避免使用它们。

你应该避免在检查两个模型是否相同的时候使用 ===。这是因为 === 检查在比较对象时将检查它们是否是同一个对象的实例。这意味着即使两个模型具有相同的数据,如果它们是不同的实例,它们也不会被认为是相同的。因此,你应该避免这样做,因为它很可能会返回 false

假设 AppModelsComment 模型上存在 post 关系,并且数据库中的第一个评论属于第一个帖子,让我们来看一个例子:

$posts = Post::with('user')->get();

foreach ($posts as $post) {
    // 对帖子执行某些操作...

    // 尝试访问帖子的用户
    echo $post->user->name;
}

你应该也避免在检查两个模型是否相同的时候使用 ==。这是因为 == 检查在比较对象时将检查它们是否是同一类的实例,以及它们是否具有相同的属性和值。但是,这可能会导致意想不到的行为。

来看这个例子:

$user = User::query()->first();

$name = $user->full_name;

在上面的示例中,== 检查将返回 true,因为 $comment->post$post 是同一类,并且具有相同的属性和值。但是,如果我们更改 $post 模型中的属性使其不同会发生什么?

让我们使用 select 方法,以便我们只从 posts 表中获取 idcontent 字段:

$posts = Post::all();

foreach ($posts as $post) {
    // 对帖子执行某些操作...

    // 尝试访问帖子的用户
    echo $post->user->name;
}

即使 $comment->post$post 是相同的模型,== 检查也将返回 false,因为模型具有不同的已加载属性。正如你所想象的那样,这可能会导致一些难以追踪的意外行为,特别是如果你已经追溯地将 select 方法添加到查询中,并且你的测试开始失败。

相反,我喜欢使用 Laravel 提供的 isisNot 方法。这些方法将比较两个模型,并检查它们是否属于同一类,是否具有相同的主键值,以及是否具有相同的数据库连接。这是一种更安全的比较模型的方法,并将有助于减少意外行为的可能性。

你可以使用 is 方法来检查两个模型是否相同:

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Model::preventLazyLoading();
    }
}

同样,你可以使用 isNot 方法来检查两个模型是否不同:

<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>

在构建查询时使用 whereBelongsTo

最后一个技巧更像是个人偏好,但我发现它使我的查询更易于阅读和理解。

在尝试从数据库中获取模型时,你可能会发现自己正在编写基于关系的过滤查询。例如,你可能希望获取属于特定用户和帖子的所有评论:

$posts = Post::with('user')->get();

foreach ($posts as $post) {
    // 对帖子执行某些操作...

    // 尝试访问帖子的用户
    echo $post->user->name;
}

Laravel 提供了一个 whereBelongsTo 方法,你可以使用它来使你的查询更易于阅读(在我看来)。使用此方法,我们可以像这样重写上面的查询:

$user = User::query()->first();

$name = $user->full_name;

我喜欢这种语法糖,并且觉得它使查询更易于阅读。这也是确保你根据正确的关系和字段进行过滤的好方法。

你或你的团队可能更喜欢使用更明确的方法来编写 where 子句。因此,这个技巧可能并不适合所有人。但我认为只要你对你的方法保持一致,这两种方法都很好。

结论

希望本文能向你展示一些使用 Laravel 模型的新技巧。你现在应该能够发现并防止 N 1 问题,防止访问缺失的属性,防止静默丢弃属性,并将主键类型更改为 UUID 或 ULID。你还应该知道如何更改用于路由模型绑定的字段,指定返回的集合类型,比较模型以及在构建查询时使用 whereBelongsTo

以上是Laravel模型提示的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn