laravel
8. Eloquent ORM
Eloquent:入门

简介

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

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

Laravel 训练营

如果您是 Laravel 的新手,欢迎参加 Laravel 训练营 (opens in a new tab)。Laravel 训练营将引导您使用 Eloquent 构建您的第一个 Laravel 应用程序。这是了解 Laravel 和 Eloquent 所提供的一切的好方法。

生成模型类

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

php artisan make:model Flight

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

php artisan make:model Flight --migration

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

# 生成一个模型和一个 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 Flight -a
 
# 生成一个枢纽模型...
php artisan make:model Member --pivot
php artisan make:model Member -p

检查模型

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

php artisan model:show Flight

Eloquent 模型约定

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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    //...
}

表名

在查看上面的示例后,您可能已经注意到我们没有告诉 Eloquent 我们的 Flight 模型对应的数据库表是什么。按照约定,除非明确指定了其他名称,否则类的“蛇形命名法”、复数名称将用作表名。因此,在这种情况下,Eloquent 将假定 Flight 模型将记录存储在 flights 表中,而 AirTrafficController 模型将记录存储在 air_traffic_controllers 表中。

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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

主键

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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

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

<?php

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

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

<?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 特征。当然,您应该确保模型具有 UUID 等效的主键列

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 特征将为您的模型生成 "有序" UUID。这些 UUID 对于索引数据库存储更有效,因为它们可以按字典顺序排序。

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

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'];
}

如果您愿意,您可以选择使用“ULID”而不是 UUID。ULID 与 UUID 类似;然而,它们的长度仅为 26 个字符。与有序 UUID 一样,ULID 可以按字典顺序排序,以便进行高效的数据库索引。要使用 ULID,您应该在模型上使用 Illuminate\Database\Eloquent\Concerns\HasUlids 特征。您还应该确保模型具有 ULID 等效的主键列

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

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

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

<?php

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

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

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

数据库连接

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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

默认属性值

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

<?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 方法中调用:

use Illuminate\Database\Eloquent\Model;
 
/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{
    Model::preventLazyLoading(! $this->app->isProduction());
}

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

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

检索模型

一旦您创建了一个模型并 创建了其相关的数据库表,您就可以开始从数据库中检索数据了。您可以将每个 Eloquent 模型视为一个强大的 查询构建器,允许您流畅地查询与模型相关联的数据库表。模型的 all 方法将从模型的相关数据库表中检索所有记录:

use App\Models\Flight;

foreach (Flight::all() as $flight) {
    echo $flight->name;
}

构建查询

Eloquent 的 all 方法将返回模型表中的所有结果。然而,由于每个 Eloquent 模型都充当一个 查询构建器,您可以向查询添加其他约束,然后调用 get 方法来检索结果:

$flights = Flight::where('active', 1)
               ->orderBy('name')
               ->take(10)
               ->get();

[!注意]
由于 Eloquent 模型是查询构建器,您应该查看 Laravel 的 查询构建器 提供的所有方法。在编写 Eloquent 查询时,您可以使用这些方法中的任何一种。

刷新模型

如果您已经从数据库中检索到了一个 Eloquent 模型的实例,您可以使用 freshrefresh 方法来“刷新”该模型。fresh 方法将从数据库中重新检索模型。现有的模型实例不会受到影响:

$flight = Flight::where('number', 'FR 900')->first();

$freshFlight = $flight->fresh();

refresh 方法将使用来自数据库的新数据重新填充现有的模型。此外,其所有加载的关系也将被刷新:

$flight = Flight::where('number', 'FR 900')->first();

$flight->number = 'FR 456';

$flight->refresh();

$flight->number; // "FR 900"

集合

如我们所见,Eloquent 方法如 allget 从数据库中检索多个记录。然而,这些方法不会返回一个普通的 PHP 数组。相反,会返回一个 Illuminate\Database\Eloquent\Collection 的实例。

Eloquent 的 Collection 类扩展了 Laravel 的基础 Illuminate\Support\Collection 类,该类为与数据集合进行交互提供了 多种有用的方法。例如,reject 方法可用于根据调用的闭包的结果从集合中删除模型:

$flights = Flight::where('destination', 'Paris')->get();
 
$flights = $flights->reject(function
### 游标(Cursors)
 
`lazy` 方法类似,当遍历数以万计的 Eloquent 模型记录时,`cursor` 方法可用于显著减少应用程序的内存消耗。
 
`cursor` 方法只会执行一个数据库查询;然而,只有在实际遍历它们时,才会水化各个 Eloquent 模型。因此,在遍历游标时,任何给定时间内只有一个 Eloquent 模型保存在内存中。
 
> [!警告]  
> 由于 `cursor` 方法在任何时候都只在内存中保存一个 Eloquent 模型,因此它无法急切加载关系。如果您需要急切加载关系,请考虑使用[`lazy` 方法](#高级子查询)代替。
 
在内部,`cursor` 方法使用 PHP [生成器](https://www.php.net/manual/en/language.generators.overview.php)来实现此功能:
 
```php
use App\Models\Flight;
 
foreach (Flight::where('destination', 'Zurich')->cursor() as $flight) {
    //...
}

cursor 返回一个 Illuminate\Support\LazyCollection 实例。懒集合允许您在每次只将一个模型加载到内存中的同时,使用许多在典型的 Laravel 集合上可用的集合方法:

use App\Models\User;
 
$users = User::cursor()->filter(function (User $user) {
    return $user->id > 500;
});
 
foreach ($users as $user) {
    echo $user->id;
}

尽管 cursor 方法比常规查询使用的内存少得多(因为每次只在内存中保存一个 Eloquent 模型),但它最终仍会耗尽内存。这是由于 PHP 的 PDO 驱动程序在其缓冲区内部缓存所有原始查询结果 (opens in a new tab)。如果您正在处理大量的 Eloquent 记录,请考虑使用lazy 方法代替。

高级子查询

子查询选择

Eloquent 还提供了高级子查询支持,允许您在单个查询中从相关表中提取信息。例如,假设我们有一个航班 destinations 表和一个飞往目的地的 flights 表。flights 表包含一个 arrived_at 列,该列指示航班到达目的地的时间。

使用查询构建器的 selectaddSelect 方法可用的子查询功能,我们可以使用单个查询选择所有 destinations 以及最近到达该目的地的航班的名称:

use App\Models\Destination;
use App\Models\Flight;

return Destination::addSelect(['last_flight' => Flight::select('name')
    ->whereColumn('destination_id', 'destinations.id')
    ->orderByDesc('arrived_at')
    ->limit(1)
])->get();

子查询排序

此外,查询构建器的 orderBy 函数支持子查询。继续使用我们的航班示例,我们可以使用此功能根据最后一个航班到达该目的地的时间对所有目的地进行排序。同样,这可以在执行单个数据库查询时完成:

return Destination::orderByDesc(
    Flight::select('arrived_at')
        ->whereColumn('destination_id', 'destinations.id')
        ->orderByDesc('arrived_at')
        ->limit(1)
)->get();

检索单个模型/聚合

除了检索与给定查询匹配的所有记录外,您还可以使用 findfirstfirstWhere 方法检索单个记录。这些方法不是返回模型的集合,而是返回一个单个模型实例:

use App\Models\Flight;

// 通过主键检索模型...
$flight = Flight::find(1);

// 检索符合查询约束的第一个模型...
$flight = Flight::where('active', 1)->first();

// 检索符合查询约束的第一个模型的替代方法...
$flight = Flight::firstWhere('active', 1);

有时您可能希望在未找到结果时执行其他操作。findOrfirstOr 方法将返回一个单个模型实例,或者,如果未找到结果,则执行给定的闭包。闭包返回的值将被视为该方法的结果:

$flight = Flight::findOr(1, function () {
    //...
});

$flight = Flight::where('legs', '>', 3)->firstOr(function () {
    //...
});

未找到异常

有时您可能希望在未找到模型时抛出异常。这在路由或控制器中特别有用。findOrFailfirstOrFail 方法将检索查询的第一个结果;但是,如果未找到结果,则会抛出 Illuminate\Database\Eloquent\ModelNotFoundException

$flight = Flight::findOrFail(1);

$flight = Flight::where('legs', '>', 3)->firstOrFail();

如果未捕获 ModelNotFoundException,则会自动向客户端发送 404 HTTP 响应:

use App\Models\Flight;

Route::get('/api/flights/{id}', function (string $id) {
    return Flight::findOrFail($id);
});

检索或创建模型

firstOrCreate 方法将尝试使用给定的列/值对定位数据库记录。如果在数据库中找不到该模型,则会插入一条记录,其属性是将第一个数组参数与可选的第二个数组参数合并的结果:

firstOrNew 方法与 firstOrCreate 类似,将尝试在数据库中查找与给定属性匹配的记录。但是,如果未找到模型,则会返回一个新的模型实例。请注意,firstOrNew 返回的模型尚未持久化到数据库中。您需要手动调用 save 方法来持久化它:

use App\Models\Flight;

// 按名称检索航班,如果不存在则创建它...
$flight = Flight::firstOrCreate([
    'name' => 'London to Paris'
]);

// 按名称检索航班,如果不存在则创建具有名称、延迟和到达时间属性的航班...
$flight = Flight::firstOrCreate(
    ['name' => 'London to Paris'],
    ['delayed' => 1, 'arrival_time' => '11:30']
);

// 按名称检索航班或实例化一个新的 Flight 实例...
$flight = Flight::firstOrNew([
    'name' => 'London to Paris'
]);

// 按名称检索航班或实例化具有名称、延迟和到达时间属性的实例...
$flight = Flight::firstOrNew(
    ['name' => 'Tokyo to Sydney'],
    ['delayed' => 1, 'arrival_time' => '11:30']
);

检索聚合

在与 Eloquent 模型交互时,您还可以使用 countsummax 和其他 聚合方法,这些方法由 Laravel 查询构建器提供。如您所料,这些方法返回标量值而不是 Eloquent 模型实例:

$count = Flight::where('active', 1)->count();

$max = Flight::where('active', 1)->max('price');

插入和更新模型

插入

当然,在使用 Eloquent 时,我们不仅需要从数据库中检索模型,还需要插入新记录。值得庆幸的是,Eloquent 使其变得简单。要将新记录插入数据库,您应该实例化一个新的模型实例并在模型上设置属性。然后,在模型实例上调用 save 方法:

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\Flight;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

class FlightController extends Controller
{
    /**
     * 在数据库中存储一个新航班。
     */
    public function store(Request $request): RedirectResponse
    {
        // 验证请求...

        $flight = new Flight;

        $flight->name = $request->name;

        $flight->save();

        return redirect('/flights');
    }
}

在这个示例中,我们将传入的 HTTP 请求中的 name 字段分配给 App\Models\Flight 模型实例的 name 属性。当我们调用 save 方法时,将向数据库中插入一条记录。当调用 save 方法时,模型的 created_atupdated_at 时间戳将自动设置,因此无需手动设置它们。

或者,您可以使用 create 方法使用单个 PHP 语句“保存”一个新模型。create 方法将返回插入的模型实例:

use App\Models\Flight;

$flight = Flight::create([
    'name' => 'London to Paris',
]);

但是,在使用 create 方法之前,您需要在模型类上指定 fillableguarded 属性。这些属性是必需的,因为默认情况下,所有 Eloquent 模型都受到防止大规模赋值漏洞的保护。要了解有关大规模赋值的更多信息,请查阅大规模赋值文档

更新

save 方法也可用于更新数据库中已存在的模型。要更新模型,您应该检索它并设置您想要更新的任何属性。然后,您应该调用模型的 save 方法。同样,updated_at 时间戳将自动更新,因此无需手动设置其值:

use App\Models\Flight;

$flight = Flight::find(1);

$flight->name = 'Paris to London';

$flight->save();

偶尔,您可能需要更新现有模型或在没有匹配模型存在的情况下创建一个新模型。与 firstOrCreate 方法类似,updateOrCreate 方法会持久化模型,因此无需手动调用 save 方法。

在下面的示例中,如果存在一个 departure 位置为 Oaklanddestination 位置为 San Diego 的航班,则其 pricediscounted 列将被更新。如果不存在这样的航班,则会创建一个新航班,其属性是将第一个参数数组与第二个参数数组合并的结果:

$flight = Flight::updateOrCreate(
    ['departure' => 'Oakland', 'destination' => 'San Diego'],
    ['price' => 99, 'discounted' => 1]
);

大规模更新

也可以对符合给定查询的模型执行更新。在这个示例中,所有 activedestinationSan Diego 的航班都将被标记为延迟:

Flight::where('active', 1)
      ->where('destination', 'San Diego')
      ->update(['delayed' => 1]);

update 方法期望一个列和值对的数组,表示应该更新的列。update 方法返回受影响的行数。

[!警告]
当通过 Eloquent 发出大规模更新时,对于更新的模型,不会触发 savingsavedupdatingupdated 模型事件。这是因为在发出大规模更新时,实际上不会检索模型。

检查属性更改

Eloquent 提供了 isDirtyisCleanwasChanged 方法来检查模型的内部状态,并确定其属性自模型最初检索以来如何发生变化。

isDirty 方法确定自模型检索以来模型的任何属性是否已更改。您可以向 isDirty 方法传递一个特定的属性名称或属性数组,以确定是否有任何属性是“脏的”。isClean 方法将确定自模型检索以来属性是否保持不变。此方法也接受一个可选的属性参数:

use App\Models\User;

$user = User::create([
    'first_name' => 'Taylor',
    'last_name' => 'Otwell',
    'title' => 'Developer',
]);

$user->title = 'Painter';

$user->isDirty(); // true
$user->isDirty('title'); // true
$user->isDirty('first_name'); // false
$user->isDirty(['first_name', 'title']); // true

$user->isClean(); // false
$user->isClean('title'); // false
$user->isClean('first_name'); // true
$user->isClean(['first_name', 'title']); // false

$user->save();

$user->isDirty(); // false
$user->isClean(); // true

wasChanged 方法确定在当前请求周期内模型最后保存时是否有任何属性发生了更改。如果需要,您可以传递一个属性名称来查看特定属性是否已更改:

$user = User::create([
    'first_name' => 'Taylor',
    'last_name' => 'Otwell',
    'title' => 'Developer',
]);

$user->title = 'Painter';

$user->save();

$user->wasChanged(); // true
$user->wasChanged('title'); // true
$user->wasChanged(['title','slug']); // true
$user->wasChanged('first_name'); // false
$user->wasChanged(['first_name', 'title']); // true

getOriginal 方法返回一个包含模型原始属性的数组,无论自检索模型以来对模型进行了任何更改。如果需要,您可以传递一个特定的属性名称来获取特定属性的原始值:

$user = User::find(1);

$user->name; // John
$user->email; // john@example.com

$user->name = "Jack";
$user->name; // Jack

$user->getOriginal('name'); // John
$user->getOriginal(); // 原始属性的数组...

大规模赋值

您可以使用 create 方法使用单个 PHP 语句“保存”一个新模型。该方法将返回插入的模型实例:

use App\Models\Flight;

$flight = Flight::create([
    'name' => 'London to Paris',
]);

但是,在使用 create 方法之前,您需要在模型类上指定 fillableguarded 属性。这些属性是必需的,因为默认情况下,所有 Eloquent 模型都受到防止大规模赋值漏洞的保护。

当用户传递一个意外的 HTTP 请求字段,并且该字段更改了您数据库中您未预期的列时,就会出现大规模赋值漏洞。例如,恶意用户可能会通过 HTTP 请求发送一个 is_admin 参数,然后该参数被传递到您的模型的 create 方法中,从而允许用户将自己提升为管理员。

因此,首先,您应该定义您希望使哪些模型属性可进行大规模赋值。您可以在模型上使用 $fillable 属性来做到这一点。例如,让我们使我们的 Flight 模型的 name 属性可进行大规模赋值:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    /**
     * 可进行大规模赋值的属性。
     *
     * @var array
     */
    protected $fillable = ['name'];
}

一旦您指定了哪些属性是可进行大规模赋值的,您就可以使用 create 方法在数据库中插入一个新记录。create 方法将返回新创建的模型实例:

$flight = Flight::create(['name' => 'London to Paris']);

如果您已经有一个模型实例,您可以使用 fill 方法用一个属性数组来填充它:

$flight->fill(['name' => 'Amsterdam to Frankfurt']);

大规模赋值和 JSON 列

在分配 JSON 列时,模型的 $fillable 数组中必须指定每个列的可大规模赋值键。为了安全起见,当使用 guarded 属性时,Laravel 不支持更新嵌套的 JSON 属性:

/**
 * 可进行大规模赋值的属性。
 *
 * @var array
 */
protected $fillable = [
    'options->enabled',
];

允许大规模赋值

如果您希望使所有属性都可进行大规模赋值,您可以将模型的 $guarded 属性定义为空数组。如果您选择取消对模型的保护,您应该特别注意始终手工制作传递给 Eloquent 的 fillcreateupdate 方法的数组:

/**
 * 不可进行大规模赋值的属性。
 *
 * @var array
 */
protected $guarded = [];

大规模赋值异常

默认情况下,在执行大规模赋值操作时,未包含在 $fillable 数组中的属性会被默默地丢弃。在生产环境中,这是预期的行为;然而,在本地开发期间,这可能会导致对模型更改未生效的原因产生困惑。

如果您愿意,您可以通过调用 preventSilentlyDiscardingAttributes 方法指示 Laravel 在尝试填充不可填充的属性时抛出异常。通常,此方法应在应用程序的 AppServiceProvider 类的 boot 方法中调用:

use Illuminate\Database\Eloquent\Model;

/**
 * 引导任何应用程序服务。
 */
public function boot(): void
{

永久删除模型

有时您可能需要真正从数据库中删除一个模型。您可以使用 forceDelete 方法从数据库表中永久删除一个软删除的模型:

$flight->forceDelete();

在构建 Eloquent 关系查询时,您也可以使用 forceDelete 方法:

$flight->history()->forceDelete();

查询软删除模型

包含软删除模型

如上所述,软删除模型将自动从查询结果中排除。但是,您可以通过在查询上调用 withTrashed 方法来强制将软删除模型包含在查询结果中:

use App\Models\Flight;

$flights = Flight::withTrashed()
                ->where('account_id', 1)
                ->get();

在构建 关系 查询时,也可以调用 withTrashed 方法:

$flight->history()->withTrashed()->get();

仅检索软删除模型

onlyTrashed 方法将仅检索软删除模型:

$flights = Flight::onlyTrashed()
                ->where('airline_id', 1)
                ->get();

修剪模型

有时您可能希望定期删除不再需要的模型。要实现此目的,您可以将 Illuminate\Database\Eloquent\PrunableIlluminate\Database\Eloquent\MassPrunable 特征添加到您希望定期修剪的模型中。在将其中一个特征添加到模型后,实现一个 prunable 方法,该方法返回一个 Eloquent 查询构建器,用于解决不再需要的模型:

<?php

namespace App\Models;

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

class Flight extends Model
{
    use Prunable;

    /**
     * 获取可修剪模型的查询。
     */
    public function prunable(): Builder
    {
        return static::where('created_at', '<=', now()->subMonth());
    }
}

当将模型标记为 Prunable 时,您还可以在模型上定义一个 pruning 方法。在模型被删除之前,将调用此方法。在从数据库中永久删除模型之前,此方法可用于删除与模型相关的任何其他资源,例如存储的文件:

/**
 * 为修剪模型做准备。
 */
protected function pruning(): void
{
    //...
}

配置好可修剪模型后,您应该在应用程序的 routes/console.php 文件中安排 model:prune Artisan 命令。您可以自由选择此命令应运行的适当间隔:

use Illuminate\Support\Facades\Schedule;

Schedule::command('model:prune')->daily();

在幕后,model:prune 命令将自动检测应用程序的 app/Models 目录中的“可修剪”模型。如果您的模型位于不同的位置,您可以使用 --model 选项指定模型类名称:

Schedule::command('model:prune', [
    '--model' => [Address::class, Flight::class],
])->daily();

如果您希望在修剪所有其他检测到的模型时排除某些模型不被修剪,您可以使用 --except 选项:

Schedule::command('model:prune', [
    '--except' => [Address::class, Flight::class],
])->daily();

您可以通过使用 --pretend 选项执行 model:prune 命令来测试您的 prunable 查询。在假装模式下,model:prune 命令将仅报告如果命令实际运行将修剪多少记录:

php artisan model:prune --pretend

[!WARNING]
如果软删除的模型符合可修剪查询,它们将被永久删除(forceDelete)。

批量修剪

当模型标记为 Illuminate\Database\Eloquent\MassPrunable 特征时,使用批量删除查询从数据库中删除模型。因此,不会调用 pruning 方法,也不会分发 deletingdeleted 模型事件。这是因为在删除之前实际上从未检索过模型,从而使修剪过程更加高效:

<?php

namespace App\Models;

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

class Flight extends Model
{
    use MassPrunable;

    /**
     * 获取可修剪模型的查询。
     */
    public function prunable(): Builder
    {
        return static::where('created_at', '<=', now()->subMonth());
    }
}

复制模型

您可以使用 replicate 方法创建现有模型实例的未保存副本。当您有许多具有相同属性的模型实例时,此方法特别有用:

use App\Models\Address;

$shipping = Address::create([
    'type' => 'shipping',
    'line_1' => '123 Example Street',
    'city' => 'Victorville',
    'state' => 'CA',
    'postcode' => '90001',
]);

$billing = $shipping->replicate()->fill([
    'type' => 'billing'
]);

$billing->save();

要从复制到新模型中排除一个或多个属性,您可以向 replicate 方法传递一个数组:

$flight = Flight::create([
    'destination' => 'LAX',
    'origin' => 'LHR',
    'last_flown' => '2020 - 03 - 04 11:00:00',
    'last_pilot_id' => 747,
]);

$flight = $flight->replicate([
    'last_flown',
    'last_pilot_id'
]);

查询范围

全局范围

全局范围允许您为给定模型的所有查询添加约束。Laravel 自身的 软删除 功能利用全局范围仅从数据库中检索“未删除”的模型。编写自己的全局范围可以提供一种方便、简单的方法,确保给定模型的每个查询都收到某些约束。

生成范围

要生成一个新的全局范围,您可以调用 make:scope Artisan 命令,该命令将把生成的范围放置在应用程序的 app/Models/Scopes 目录中:

php artisan make:scope AncientScope

编写全局范围

编写全局范围很简单。首先,使用 make:scope 命令生成一个实现 Illuminate\Database\Eloquent\Scope 接口的类。Scope 接口要求您实现一个方法:applyapply 方法可以根据需要向查询添加 where 约束或其他类型的子句:

<?php

namespace App\Models\Scopes;

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

class AncientScope implements Scope
{
    /**
     * 将范围应用于给定的 Eloquent 查询构建器。
     */
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('created_at', '<', now()->subYears(2000));
    }
}

[!NOTE]
如果您的全局范围是向查询的选择子句中添加列,则应使用 addSelect 方法而不是 select。这将防止意外替换查询的现有选择子句。

应用全局范围

要将全局范围分配给模型,您可以简单地将 ScopedBy 属性放在模型上:

<?php

namespace App\Models;

use App\Models\Scopes\AncientScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;

#[ScopedBy([AncientScope::class])]
class User extends Model
{
    //
}

或者,您可以通过重写模型的 booted 方法并调用模型的 addGlobalScope 方法手动注册全局范围。addGlobalScope 方法接受您的范围的实例作为其唯一参数:

<?php

namespace App\Models;

use App\Models\Scopes\AncientScope;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * 模型的“已启动”方法。
     */
    protected static function booted(): void
    {
        static::addGlobalScope(new AncientScope);
    }
}

在将上述示例中的范围添加到 App\Models\User 模型后,对 User::all() 方法的调用将执行以下 SQL 查询:

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

匿名全局范围

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

<?php

namespace App\Models;

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

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

移除全局范围

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

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

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

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

如果您想删除查询的几个甚至所有全局范围,可以使用 withoutGlobalScopes 方法:

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

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

局部范围

局部范围允许您定义在整个应用程序中可以轻松重复使用的常见查询约束集。例如,您可能需要经常检索所有被认为是“受欢迎”的用户。要定义一个范围,在 Eloquent 模型方法前加上 scope 前缀。

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

<?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 前缀。您甚至可以将对各种范围的调用链接起来:

use App\Models\User;

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

通过 or 查询操作符将多个 Eloquent 模型范围组合在一起可能需要使用闭包来实现正确的 逻辑分组

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

然而,由于这可能很麻烦,Laravel 提供了一个“高阶” orWhere 方法,允许您在不使用闭包的情况下流畅地将范围链接在一起:

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

动态范围

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

<?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);
    }
}

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

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

比较模型

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

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

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

当使用 belongsTohasOnemorphTomorphOne 关系 时,isisNot 方法也可用。当您想要比较相关模型而无需发出查询来检索该模型时,此方法特别有用:

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

事件

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

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

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

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

<?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<string, string>
     */
    protected $dispatchesEvents = [
        'saved' => UserSaved::class,
        'deleted' => UserDeleted::class,
    ];
}

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

[!WARNING]
当通过 Eloquent 发出批量更新或删除查询时,对于受影响的模型,不会分发 savedupdateddeletingdeleted 模型事件。这是因为在执行批量更新或删除时,实际上从未检索过模型。

使用闭包

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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

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

use function Illuminate\Events\queueable;

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

观察者