Skip to content

Eloquent:入门

介绍

Laravel 包含 Eloquent,这是一个对象关系映射器(ORM),使与数据库的交互变得愉快。使用 Eloquent 时,每个数据库表都有一个对应的 "模型",用于与该表交互。除了从数据库表中检索记录外,Eloquent 模型还允许您插入、更新和删除表中的记录。

lightbulb

在开始之前,请确保在应用程序的 config/database.php 配置文件中配置数据库连接。有关配置数据库的更多信息,请查看 数据库配置文档

Laravel Bootcamp

如果您是 Laravel 新手,可以随时进入 Laravel Bootcamp。Laravel Bootcamp 将引导您使用 Eloquent 构建第一个 Laravel 应用程序。这是一个了解 Laravel 和 Eloquent 所提供的一切的好方法。

生成模型类

首先,让我们创建一个 Eloquent 模型。模型通常位于 app\Models 目录中,并扩展 Illuminate\Database\Eloquent\Model 类。您可以使用 make:model Artisan 命令 生成新模型:

shell
php artisan make:model Flight

如果您希望在生成模型时生成 数据库迁移,可以使用 --migration-m 选项:

shell
php artisan make:model Flight --migration

生成模型时,您可以生成各种其他类型的类,例如工厂、填充器、策略、控制器和表单请求。此外,这些选项可以组合在一起以一次生成多个类:

shell
# 生成模型和 FlightFactory 类...
php artisan make:model Flight --factory
php artisan make:model Flight -f

# 生成模型和 FlightSeeder 类...
php artisan make:model Flight --seed
php artisan make:model Flight -s

# 生成模型和 FlightController 类...
php artisan make:model Flight --controller
php artisan make:model Flight -c

# 生成模型、FlightController 资源类和表单请求类...
php artisan make:model Flight --controller --resource --requests
php artisan make:model Flight -crR

# 生成模型和 FlightPolicy 类...
php artisan make:model Flight --policy

# 生成模型和迁移、工厂、填充器和控制器...
php artisan make:model Flight -mfsc

# 快捷方式生成模型、迁移、工厂、填充器、策略、控制器和表单请求...
php artisan make:model Flight --all

# 生成枢纽模型...
php artisan make:model Member --pivot
php artisan make:model Member -p

检查模型

有时,仅通过浏览代码很难确定模型的所有可用属性和关系。相反,尝试使用 model:show Artisan 命令,该命令提供了模型所有属性和关系的便捷概述:

shell
php artisan model:show Flight

Eloquent 模型约定

通过 make:model 命令生成的模型将放置在 app/Models 目录中。让我们检查一个基本的模型类,并讨论一些 Eloquent 的关键约定:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    // ...
}

表名

在浏览上面的示例后,您可能注意到我们没有告诉 Eloquent 哪个数据库表对应于我们的 Flight 模型。按照惯例,类的 "蛇形命名法" 复数名称将用作表名,除非明确指定了其他名称。因此,在这种情况下,Eloquent 将假定 Flight 模型将记录存储在 flights 表中,而 AirTrafficController 模型将记录存储在 air_traffic_controllers 表中。

如果模型对应的数据库表不符合此约定,您可以通过在模型上定义 table 属性手动指定模型的表名:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    /**
     * 与模型关联的表。
     *
     * @var string
     */
    protected $table = 'my_flights';
}

主键

Eloquent 还假定每个模型对应的数据库表都有一个名为 id 的主键列。如果需要,您可以在模型上定义一个受保护的 $primaryKey 属性,以指定用作模型主键的不同列:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    /**
     * 与表关联的主键。
     *
     * @var string
     */
    protected $primaryKey = 'flight_id';
}

此外,Eloquent 假定主键是递增的整数值,这意味着 Eloquent 将自动将主键转换为整数。如果您希望使用非递增或非数字主键,必须在模型上定义一个公共 $incrementing 属性,并将其设置为 false

php
<?php

class Flight extends Model
{
    /**
     * 指示模型的 ID 是否自动递增。
     *
     * @var bool
     */
    public $incrementing = false;
}

如果模型的主键不是整数,您应在模型上定义一个受保护的 $keyType 属性。此属性的值应为 string

php
<?php

class Flight extends Model
{
    /**
     * 主键 ID 的数据类型。
     *
     * @var string
     */
    protected $keyType = 'string';
}

"复合" 主键

Eloquent 要求每个模型至少有一个唯一标识的 "ID",可以用作其主键。Eloquent 模型不支持 "复合" 主键。但是,您可以在数据库表中添加其他多列唯一索引,除了表的唯一标识主键之外。

UUID 和 ULID 键

您可以选择使用 UUID 而不是自动递增整数作为 Eloquent 模型的主键。UUID 是 36 个字符长的全局唯一字母数字标识符。

如果您希望模型使用 UUID 键而不是自动递增整数键,可以在模型上使用 Illuminate\Database\Eloquent\Concerns\HasUuids trait。当然,您应确保模型具有 UUID 等效主键列

php
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    use HasUuids;

    // ...
}

$article = Article::create(['title' => 'Traveling to Europe']);

$article->id; // "8f8e8478-9035-4d23-b9a7-62f4d2612ce5"

默认情况下,HasUuids trait 将为您的模型生成 "有序" UUID。这些 UUID 对于索引数据库存储更有效,因为它们可以按字典顺序排序。

您可以通过在模型上定义 newUniqueId 方法来覆盖给定模型的 UUID 生成过程。此外,您可以通过在模型上定义 uniqueIds 方法来指定哪些列应接收 UUID:

php
use Ramsey\Uuid\Uuid;

/**
 * 为模型生成新的 UUID。
 */
public function newUniqueId(): string
{
    return (string) Uuid::uuid4();
}

/**
 * 获取应接收唯一标识符的列。
 *
 * @return array<int, string>
 */
public function uniqueIds(): array
{
    return ['id', 'discount_code'];
}

如果您愿意,可以选择使用 "ULIDs" 而不是 UUID。ULIDs 类似于 UUID;然而,它们只有 26 个字符长。与有序 UUID 一样,ULIDs 可以按字典顺序排序,以实现高效的数据库索引。要使用 ULIDs,您应在模型上使用 Illuminate\Database\Eloquent\Concerns\HasUlids trait。您还应确保模型具有 ULID 等效主键列

php
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    use HasUlids;

    // ...
}

$article = Article::create(['title' => 'Traveling to Asia']);

$article->id; // "01gd4d3tgrrfqeda94gdbtdk5c"

时间戳

默认情况下,Eloquent 期望在模型对应的数据库表中存在 created_atupdated_at 列。Eloquent 将在创建或更新模型时自动设置这些列的值。如果您不希望这些列由 Eloquent 自动管理,应在模型上定义一个 $timestamps 属性,并将其值设置为 false

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    /**
     * 指示模型是否应使用时间戳。
     *
     * @var bool
     */
    public $timestamps = false;
}

如果需要自定义模型时间戳的格式,请在模型上设置 $dateFormat 属性。此属性确定日期属性在数据库中的存储方式以及模型序列化为数组或 JSON 时的格式:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    /**
     * 模型日期列的存储格式。
     *
     * @var string
     */
    protected $dateFormat = 'U';
}

如果需要自定义用于存储时间戳的列的名称,可以在模型上定义 CREATED_ATUPDATED_AT 常量:

php
<?php

class Flight extends Model
{
    const CREATED_AT = 'creation_date';
    const UPDATED_AT = 'updated_date';
}

如果您希望在不修改模型的 updated_at 时间戳的情况下执行模型操作,可以在传递给 withoutTimestamps 方法的闭包中操作模型:

php
Model::withoutTimestamps(fn () => $post->increment(['reads']));

数据库连接

默认情况下,所有 Eloquent 模型将使用为应用程序配置的默认数据库连接。如果您希望指定在与特定模型交互时应使用的不同连接,应在模型上定义一个 $connection 属性:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    /**
     * 模型应使用的数据库连接。
     *
     * @var string
     */
    protected $connection = 'sqlite';
}

默认属性值

默认情况下,新实例化的模型实例不包含任何属性值。如果您希望为模型的某些属性定义默认值,可以在模型上定义一个 $attributes 属性。放置在 $attributes 数组中的属性值应为其原始的、"可存储" 的格式,就像它们刚从数据库中读取一样:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    /**
     * 模型的默认属性值。
     *
     * @var array
     */
    protected $attributes = [
        'options' => '[]',
        'delayed' => false,
    ];
}

配置 Eloquent 严格性

Laravel 提供了几种方法,允许您在各种情况下配置 Eloquent 的行为和 "严格性"。

首先,preventLazyLoading 方法接受一个可选的布尔参数,指示是否应防止惰性加载。例如,您可能希望仅在非生产环境中禁用惰性加载,以便即使在生产代码中意外存在惰性加载关系,生产环境也能正常运行。通常,此方法应在应用程序的 AppServiceProviderboot 方法中调用:

php
use Illuminate\Database\Eloquent\Model;

/**
 * 启动任何应用程序服务。
 */
public function boot(): void
{
    Model::preventLazyLoading(! $this->app->isProduction());
}

此外,您可以通过调用 preventSilentlyDiscardingAttributes 方法指示 Laravel 在尝试填充不可填充属性时抛出异常。这可以帮助防止在本地开发期间尝试设置未添加到模型的 fillable 数组中的属性时出现意外错误:

php
Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());

检索模型

一旦您创建了模型和 其关联的数据库表,您就可以开始从数据库中检索数据。您可以将每个 Eloquent 模型视为一个强大的 查询构建器,允许您流畅地查询与模型关联的数据库表。模型的 all 方法将检索模型关联的数据库表中的所有记录: ...(about 1451 lines omitted)... 在上面的示例中将范围添加到 App\Models\User 模型后,调用 User::all() 方法将执行以下 SQL 查询:

sql
select * from `users` where `created_at` < 0021-02-18 00:00:00

匿名全局范围

Eloquent 还允许您使用闭包定义全局范围,这对于不需要单独类的简单范围特别有用。使用闭包定义全局范围时,您应提供一个自选的范围名称作为 addGlobalScope 方法的第一个参数:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * 模型的 "booted" 方法。
     */
    protected static function booted(): void
    {
        static::addGlobalScope('ancient', function (Builder $builder) {
            $builder->where('created_at', '<', now()->subYears(2000));
        });
    }
}

移除全局范围

如果您希望为给定查询移除全局范围,可以使用 withoutGlobalScope 方法。此方法接受全局范围的类名作为其唯一参数:

php
User::withoutGlobalScope(AncientScope::class)->get();

或者,如果您使用闭包定义了全局范围,应传递您分配给全局范围的字符串名称:

php
User::withoutGlobalScope('ancient')->get();

如果您希望移除多个甚至所有查询的全局范围,可以使用 withoutGlobalScopes 方法:

php
// 移除所有全局范围...
User::withoutGlobalScopes()->get();

// 移除一些全局范围...
User::withoutGlobalScopes([
    FirstScope::class, SecondScope::class
])->get();

本地范围

本地范围允许您定义常见的查询约束集,您可以在整个应用程序中轻松重用。例如,您可能需要经常检索所有被认为是 "热门" 的用户。要定义范围,请在 Eloquent 模型方法前加上 scope

范围应始终返回相同的查询构建器实例或 void

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * 将查询范围限定为仅包含热门用户。
     */
    public function scopePopular(Builder $query): void
    {
        $query->where('votes', '>', 100);
    }

    /**
     * 将查询范围限定为仅包含活跃用户。
     */
    public function scopeActive(Builder $query): void
    {
        $query->where('active', 1);
    }
}

利用本地范围

定义范围后,您可以在查询模型时调用范围方法。但是,调用方法时不应包含 scope 前缀。您甚至可以链式调用各种范围:

php
use App\Models\User;

$users = User::popular()->active()->orderBy('created_at')->get();

通过 or 查询运算符组合多个 Eloquent 模型范围可能需要使用闭包以实现正确的 逻辑分组

php
$users = User::popular()->orWhere(function (Builder $query) {
    $query->active();
})->get();

然而,由于这可能很麻烦,Laravel 提供了一个 "高阶" orWhere 方法,允许您流畅地将范围链在一起,而无需使用闭包:

php
$users = User::popular()->orWhere->active()->get();

动态范围

有时您可能希望定义一个接受参数的范围。要开始,只需将附加参数添加到范围方法的签名中。范围参数应在 $query 参数之后定义:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * 将查询范围限定为仅包含给定类型的用户。
     */
    public function scopeOfType(Builder $query, string $type): void
    {
        $query->where('type', $type);
    }
}

一旦将预期参数添加到范围方法的签名中,您可以在调用范围时传递参数:

php
$users = User::ofType('admin')->get();

比较模型

有时您可能需要确定两个模型是否 "相同"。isisNot 方法可用于快速验证两个模型是否具有相同的主键、表和数据库连接:

php
if ($post->is($anotherPost)) {
    // ...
}

if ($post->isNot($anotherPost)) {
    // ...
}

在使用 belongsTohasOnemorphTomorphOne 关系 时,isisNot 方法也可用。当您希望在不发出查询以检索相关模型的情况下比较相关模型时,此方法特别有用:

php
if ($post->author()->is($user)) {
    // ...
}

事件

lightbulb

想要将 Eloquent 事件直接广播到客户端应用程序?查看 Laravel 的 模型事件广播

Eloquent 模型会调度多个事件,允许您在模型生命周期的以下时刻进行挂钩:retrievedcreatingcreatedupdatingupdatedsavingsaveddeletingdeletedtrashedforceDeletingforceDeletedrestoringrestoredreplicating

当从数据库中检索现有模型时,将调度 retrieved 事件。当首次保存新模型时,将调度 creatingcreated 事件。当修改现有模型并调用 save 方法时,将调度 updating / updated 事件。当创建或更新模型时,将调度 saving / saved 事件 - 即使模型的属性未更改。以 -ing 结尾的事件在对模型的更改持久化之前调度,而以 -ed 结尾的事件在对模型的更改持久化之后调度。

要开始监听模型事件,请在 Eloquent 模型上定义一个 $dispatchesEvents 属性。此属性将 Eloquent 模型生命周期的各个点映射到您自己的 事件类。每个模型事件类应期望通过其构造函数接收受影响模型的实例:

php
<?php

namespace App\Models;

use App\Events\UserDeleted;
use App\Events\UserSaved;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * 模型的事件映射。
     *
     * @var array
     */
    protected $dispatchesEvents = [
        'saved' => UserSaved::class,
        'deleted' => UserDeleted::class,
    ];
}

在定义和映射 Eloquent 事件后,您可以使用 事件监听器 处理事件。

exclamation

通过 Eloquent 发出批量更新或删除查询时,不会为受影响的模型调度 savedupdateddeletingdeleted 模型事件。这是因为在执行批量更新或删除时,模型实际上从未被检索。

使用闭包

您可以注册在调度各种模型事件时执行的闭包,而不是使用自定义事件类。通常,您应在模型的 booted 方法中注册这些闭包:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * 模型的 "booted" 方法。
     */
    protected static function booted(): void
    {
        static::created(function (User $user) {
            // ...
        });
    }
}

如果需要,您可以在注册模型事件时使用 可排队的匿名事件监听器。这将指示 Laravel 使用应用程序的 队列 在后台执行模型事件监听器:

php
use function Illuminate\Events\queueable;

static::created(queueable(function (User $user) {
    // ...
}));

观察者

定义观察者

如果您正在监听给定模型的许多事件,可以使用观察者将所有监听器分组到一个类中。观察者类的方法名称反映了您希望监听的 Eloquent 事件。每个方法接收受影响的模型作为其唯一参数。make:observer Artisan 命令是创建新观察者类的最简单方法:

shell
php artisan make:observer UserObserver --model=User

此命令会将新观察者放置在 app/Observers 目录中。如果此目录不存在,Artisan 将为您创建它。您的新观察者将如下所示:

php
<?php

namespace App\Observers;

use App\Models\User;

class UserObserver
{
    /**
     * 处理 User "created" 事件。
     */
    public function created(User $user): void
    {
        // ...
    }

    /**
     * 处理 User "updated" 事件。
     */
    public function updated(User $user): void
    {
        // ...
    }

    /**
     * 处理 User "deleted" 事件。
     */
    public function deleted(User $user): void
    {
        // ...
    }

    /**
     * 处理 User "restored" 事件。
     */
    public function restored(User $user): void
    {
        // ...
    }

    /**
     * 处理 User "forceDeleted" 事件。
     */
    public function forceDeleted(User $user): void
    {
        // ...
    }
}

要注册观察者,您可以在相应的模型上放置 ObservedBy 属性:

php
use App\Observers\UserObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;

#[ObservedBy([UserObserver::class])]
class User extends Authenticatable
{
    //
}

或者,您可以通过调用要观察的模型上的 observe 方法手动注册观察者。您可以在应用程序的 App\Providers\EventServiceProvider 服务提供者的 boot 方法中注册观察者:

php
use App\Models\User;
use App\Observers\UserObserver;

/**
 * 为应用程序注册任何事件。
 */
public function boot(): void
{
    User::observe(UserObserver::class);
}
lightbulb

观察者可以监听其他事件,例如 savingretrieved。这些事件在 事件 文档中进行了描述。

观察者和数据库事务

当模型在数据库事务中创建时,您可能希望指示观察者仅在数据库事务提交后执行其事件处理程序。您可以通过在观察者上实现 ShouldHandleEventsAfterCommit 接口来实现此目的。如果数据库事务未进行中,事件处理程序将立即执行:

php
<?php

namespace App\Observers;

use App\Models\User;
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;

class UserObserver implements ShouldHandleEventsAfterCommit
{
    /**
     * 处理 User "created" 事件。
     */
    public function created(User $user): void
    {
        // ...
    }
}

静音事件

您可能偶尔需要暂时 "静音" 模型触发的所有事件。您可以使用 withoutEvents 方法实现此目的。withoutEvents 方法接受一个闭包作为其唯一参数。在此闭包中执行的任何代码都不会触发模型事件,闭包返回的任何值都将由 withoutEvents 方法返回:

php
use App\Models\User;

$user = User::withoutEvents(function () {
    User::findOrFail(1)->delete();

    return User::find(2);
});

保存单个模型而不触发事件

有时您可能希望在不触发任何事件的情况下 "保存" 给定模型。您可以使用 saveQuietly 方法实现此目的:

php
$user = User::findOrFail(1);

$user->name = 'Victoria Faith';

$user->saveQuietly();

您还可以在不触发任何事件的情况下 "更新"、"删除"、"软删除"、"恢复" 和 "复制" 给定模型:

php
$user->deleteQuietly();
$user->forceDeleteQuietly();
$user->restoreQuietly();