跳转到内容

Eloquent:入门

简介

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

lightbulb

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

Laravel 训练营

如果您是 Laravel 的新手,请随时加入 Laravel 训练营。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

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

# Generate a model and a FlightFactory class...
php artisan make:model Flight --factory
php artisan make:model Flight -f
 
# Generate a model and a FlightSeeder class...
php artisan make:model Flight --seed
php artisan make:model Flight -s
 
# Generate a model and a FlightController class...
php artisan make:model Flight --controller
php artisan make:model Flight -c
 
# Generate a model, FlightController resource class, and form request classes...
php artisan make:model Flight --controller --resource --requests
php artisan make:model Flight -crR
 
# Generate a model and a FlightPolicy class...
php artisan make:model Flight --policy
 
# Generate a model and a migration, factory, seeder, and controller...
php artisan make:model Flight -mfsc
 
# Shortcut to generate a model, migration, factory, seeder, policy, controller, and form requests...
php artisan make:model Flight --all
php artisan make:model Flight -a
 
# Generate a pivot model...
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
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'my_flights';
}

主键

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

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
 
class Flight extends Model
{
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'flight_id';
}

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

<?php
 
class Flight extends Model
{
/**
* Indicates if the model's ID is auto-incrementing.
*
* @var bool
*/
public $incrementing = false;
}

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

<?php
 
class Flight extends Model
{
/**
* The data type of the primary key ID.
*
* @var string
*/
protected $keyType = 'string';
}

“复合”主键

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

UUID 和 ULID 键

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

如果您希望模型使用 UUID 键而不是自增整数键,您可以在模型上使用 Illuminate\Database\Eloquent\Concerns\HasUuids trait。当然,您应该确保该模型具有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 trait 将为您的模型生成“有序” UUID。这些 UUID 对于索引数据库存储更有效,因为它们可以按字典顺序排序。

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

use Ramsey\Uuid\Uuid;
 
/**
* Generate a new UUID for the model.
*/
public function newUniqueId(): string
{
return (string) Uuid::uuid4();
}
 
/**
* Get the columns that should receive a unique identifier.
*
* @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 trait。您还应该确保模型具有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 自动管理,则应在模型上定义一个值为 false$timestamps 属性

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
 
class Flight extends Model
{
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
}

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

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
 
class Flight extends Model
{
/**
* The storage format of the model's date columns.
*
* @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
{
/**
* The database connection that should be used by the model.
*
* @var string
*/
protected $connection = 'mysql';
}

默认属性值

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

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
 
class Flight extends Model
{
/**
* The model's default values for attributes.
*
* @var array
*/
protected $attributes = [
'options' => '[]',
'delayed' => false,
];
}

配置 Eloquent 的严格模式

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

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

use Illuminate\Database\Eloquent\Model;
 
/**
* Bootstrap any application services.
*/
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();
lightbulb

由于 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"

集合

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

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

$flights = Flight::where('destination', 'Paris')->get();
 
$flights = $flights->reject(function (Flight $flight) {
return $flight->cancelled;
});

除了 Laravel 的基础集合类提供的方法之外,Eloquent 集合类还提供了一些额外的方法,这些方法专门用于与 Eloquent 模型集合进行交互。

由于 Laravel 的所有集合都实现了 PHP 的可迭代接口,你可以像遍历数组一样遍历集合。

foreach ($flights as $flight) {
echo $flight->name;
}

分块结果

如果尝试通过 allget 方法加载数万条 Eloquent 记录,你的应用程序可能会耗尽内存。可以使用 chunk 方法来更有效地处理大量模型,而不是使用这些方法。

chunk 方法将检索 Eloquent 模型的一个子集,并将其传递给闭包进行处理。由于一次只检索当前块的 Eloquent 模型,因此在处理大量模型时,chunk 方法将显著减少内存使用量。

use App\Models\Flight;
use Illuminate\Database\Eloquent\Collection;
 
Flight::chunk(200, function (Collection $flights) {
foreach ($flights as $flight) {
// ...
}
});

传递给 chunk 方法的第一个参数是你希望每个“块”接收的记录数。传递作为第二个参数的闭包将为从数据库检索的每个块调用。将执行数据库查询以检索传递给闭包的每个块的记录。

如果你正在根据在迭代结果时也将更新的列来筛选 chunk 方法的结果,则应使用 chunkById 方法。在这些情况下使用 chunk 方法可能会导致意外和不一致的结果。在内部,chunkById 方法将始终检索 id 列大于前一个块中最后一个模型的模型。

Flight::where('departed', true)
->chunkById(200, function (Collection $flights) {
$flights->each->update(['departed' => false]);
}, column: 'id');

由于 chunkByIdlazyById 方法将它们自己的 “where” 条件添加到正在执行的查询中,你通常应该在闭包中逻辑分组你自己的条件。

Flight::where(function ($query) {
$query->where('delayed', true)->orWhere('cancelled', true);
})->chunkById(200, function (Collection $flights) {
$flights->each->update([
'departed' => false,
'cancelled' => true
]);
}, column: 'id');

使用惰性集合分块

lazy 方法的工作方式与chunk 方法类似,因为在幕后,它会分块执行查询。但是,lazy 方法不是像那样直接将每个块传递给回调,而是返回一个扁平化的 Eloquent 模型LazyCollection,这使你可以将结果作为单个流进行交互。

use App\Models\Flight;
 
foreach (Flight::lazy() as $flight) {
// ...
}

如果你正在根据在迭代结果时也将更新的列来筛选 lazy 方法的结果,则应使用 lazyById 方法。在内部,lazyById 方法将始终检索 id 列大于前一个块中最后一个模型的模型。

Flight::where('departed', true)
->lazyById(200, column: 'id')
->each->update(['departed' => false]);

你可以使用 lazyByIdDesc 方法根据 id 的降序对结果进行筛选。

游标

lazy 方法类似,cursor 方法可用于在迭代数万条 Eloquent 模型记录时显著减少应用程序的内存消耗。

cursor 方法只会执行一个数据库查询;但是,在实际迭代之前,不会水合各个 Eloquent 模型。因此,在迭代游标时,任何给定时间都只有一个 Eloquent 模型保留在内存中。

exclamation

由于 cursor 方法一次只将单个 Eloquent 模型保存在内存中,因此它无法预先加载关系。如果你需要预先加载关系,请考虑使用lazy 方法

在内部,cursor 方法使用 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 驱动程序在其缓冲区中内部缓存所有原始查询结果。如果你正在处理大量 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;
 
// Retrieve a model by its primary key...
$flight = Flight::find(1);
 
// Retrieve the first model matching the query constraints...
$flight = Flight::where('active', 1)->first();
 
// Alternative to retrieving the first model matching the query constraints...
$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 方法将尝试使用给定的列/值对查找数据库记录。如果在数据库中找不到该模型,则将插入一条记录,该记录的属性是通过将第一个数组参数与可选的第二个数组参数合并而生成的。

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

use App\Models\Flight;
 
// Retrieve flight by name or create it if it doesn't exist...
$flight = Flight::firstOrCreate([
'name' => 'London to Paris'
]);
 
// Retrieve flight by name or create it with the name, delayed, and arrival_time attributes...
$flight = Flight::firstOrCreate(
['name' => 'London to Paris'],
['delayed' => 1, 'arrival_time' => '11:30']
);
 
// Retrieve flight by name or instantiate a new Flight instance...
$flight = Flight::firstOrNew([
'name' => 'London to Paris'
]);
 
// Retrieve flight by name or instantiate with the name, delayed, and arrival_time attributes...
$flight = Flight::firstOrNew(
['name' => 'Tokyo to Sydney'],
['delayed' => 1, 'arrival_time' => '11:30']
);

检索聚合

与 Eloquent 模型交互时,你还可以使用 Laravel 查询构建器提供的 countsummax 和其他聚合方法。正如你可能预料的那样,这些方法返回一个标量值,而不是 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
{
/**
* Store a new flight in the database.
*/
public function store(Request $request): RedirectResponse
{
// Validate the request...
 
$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 方法返回受影响的行数。

exclamation

当通过 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; // [email protected]
 
$user->name = "Jack";
$user->name; // Jack
 
$user->getOriginal('name'); // John
$user->getOriginal(); // Array of original attributes...

批量赋值

你可以使用 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
{
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
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 属性。

/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'options->enabled',
];

允许批量赋值

如果你希望使所有属性都允许批量赋值,可以将模型的 $guarded 属性定义为空数组。如果你选择取消保护模型,你应该特别注意始终手动创建传递给 Eloquent 的 fillcreateupdate 方法的数组。

/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = [];

批量赋值异常

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

如果你希望 Laravel 在尝试填充不可填充的属性时引发异常,可以调用 preventSilentlyDiscardingAttributes 方法。通常,应在应用程序的 AppServiceProvider 类的 boot 方法中调用此方法。

use Illuminate\Database\Eloquent\Model;
 
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Model::preventSilentlyDiscardingAttributes($this->app->isLocal());
}

Upserts

Eloquent 的 upsert 方法可用于在单个原子操作中更新或创建记录。该方法的第一个参数包含要插入或更新的值,而第二个参数列出唯一标识关联表中记录的列。该方法的第三个也是最后一个参数是一个数组,其中包含如果数据库中已存在匹配的记录时应更新的列。如果模型启用了时间戳,则 upsert 方法将自动设置 created_atupdated_at 时间戳。

Flight::upsert([
['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99],
['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150]
], uniqueBy: ['departure', 'destination'], update: ['price']);
exclamation

除了 SQL Server 之外,所有数据库都要求 upsert 方法的第二个参数中的列具有“主”索引或“唯一”索引。此外,MariaDB 和 MySQL 数据库驱动程序会忽略 upsert 方法的第二个参数,并始终使用表中的“主”索引和“唯一”索引来检测现有记录。

删除模型

要删除模型,可以在模型实例上调用 delete 方法。

use App\Models\Flight;
 
$flight = Flight::find(1);
 
$flight->delete();

你可以调用 truncate 方法来删除模型的所有关联数据库记录。truncate 操作还会重置模型关联表上的任何自动递增 ID。

Flight::truncate();

通过主键删除现有模型

在上面的示例中,我们在调用 delete 方法之前从数据库中检索模型。但是,如果你知道模型的主键,则可以通过调用 destroy 方法来删除模型,而无需显式检索它。除了接受单个主键之外,destroy 方法还将接受多个主键、主键数组或主键的 集合

Flight::destroy(1);
 
Flight::destroy(1, 2, 3);
 
Flight::destroy([1, 2, 3]);
 
Flight::destroy(collect([1, 2, 3]));

如果你正在使用软删除模型,则可以通过 forceDestroy 方法永久删除模型。

Flight::forceDestroy(1);
exclamation

destroy 方法会单独加载每个模型并调用 delete 方法,以便为每个模型正确分派 deletingdeleted 事件。

使用查询删除模型

当然,你可以构建一个 Eloquent 查询来删除所有与你的查询条件匹配的模型。在此示例中,我们将删除所有标记为非活动的航班。与批量更新类似,批量删除不会为已删除的模型分派模型事件。

$deleted = Flight::where('active', 0)->delete();
exclamation

当通过 Eloquent 执行批量删除语句时,不会为已删除的模型分派 deletingdeleted 模型事件。这是因为在执行删除语句时,实际上不会检索模型。

软删除

除了实际从数据库中删除记录外,Eloquent 还可以“软删除”模型。当模型被软删除时,实际上不会从数据库中删除它们。相反,会在模型上设置一个 deleted_at 属性,指示模型被“删除”的日期和时间。要为模型启用软删除,请将 Illuminate\Database\Eloquent\SoftDeletes trait 添加到模型中。

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
 
class Flight extends Model
{
use SoftDeletes;
}
lightbulb

SoftDeletes trait 将自动将 deleted_at 属性转换为 DateTime / Carbon 实例。

你还应该将 deleted_at 列添加到数据库表中。Laravel 模式生成器包含一个帮助方法来创建此列。

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
 
Schema::table('flights', function (Blueprint $table) {
$table->softDeletes();
});
 
Schema::table('flights', function (Blueprint $table) {
$table->dropSoftDeletes();
});

现在,当你调用模型的 delete 方法时,deleted_at 列将被设置为当前日期和时间。但是,模型的数据库记录将保留在表中。查询使用软删除的模型时,软删除的模型将自动从所有查询结果中排除。

要确定给定的模型实例是否已被软删除,可以使用 trashed 方法。

if ($flight->trashed()) {
// ...
}

恢复软删除的模型

有时,你可能希望“取消删除”软删除的模型。要恢复软删除的模型,可以在模型实例上调用 restore 方法。restore 方法会将模型的 deleted_at 列设置为 null

$flight->restore();

你还可以在查询中使用 restore 方法来恢复多个模型。同样,与其他“批量”操作类似,这不会为恢复的模型分派任何模型事件。

Flight::withTrashed()
->where('airline_id', 1)
->restore();

在构建 关系 查询时,也可以使用 restore 方法。

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

永久删除模型

有时,你可能需要真正地从数据库中删除模型。你可以使用 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 trait 添加到你想要定期修剪的模型中。将其中一个 trait 添加到模型后,实现一个 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;
 
/**
* Get the prunable model query.
*/
public function prunable(): Builder
{
return static::where('created_at', '<=', now()->subMonth());
}
}

在将模型标记为 Prunable 时,你还可以在模型上定义一个 pruning 方法。此方法将在删除模型之前调用。此方法对于删除与模型关联的任何其他资源(例如存储的文件)非常有用,然后再从数据库中永久删除该模型。

/**
* Prepare the model for 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 目录中的“Prunable”模型。如果你的模型位于不同的位置,则可以使用 --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
exclamation

如果软删除的模型与可修剪查询匹配,则将永久删除(forceDelete)。

批量修剪

当模型标记了 Illuminate\Database\Eloquent\MassPrunable trait 时,模型会使用批量删除查询从数据库中删除。因此,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;
 
/**
* Get the prunable model query.
*/
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
{
/**
* Apply the scope to a given Eloquent query builder.
*/
public function apply(Builder $builder, Model $model): void
{
$builder->where('created_at', '<', now()->subYears(2000));
}
}
lightbulb

如果您的全局作用域正在向查询的 select 子句添加列,您应该使用 addSelect 方法而不是 select。这将防止意外替换查询现有的 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
{
/**
* The "booted" method of the 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
{
/**
* The "booted" method of the 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 方法。

// Remove all of the global scopes...
User::withoutGlobalScopes()->get();
 
// Remove some of the global scopes...
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
{
/**
* Scope a query to only include popular users.
*/
public function scopePopular(Builder $query): void
{
$query->where('votes', '>', 100);
}
 
/**
* Scope a query to only include active users.
*/
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
{
/**
* Scope a query to only include users of a given type.
*/
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)) {
// ...
}

事件

lightbulb

想将您的 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;
 
/**
* The event map for the model.
*
* @var array<string, string>
*/
protected $dispatchesEvents = [
'saved' => UserSaved::class,
'deleted' => UserDeleted::class,
];
}

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

exclamation

当通过 Eloquent 发出批量更新或删除查询时,不会为受影响的模型分发 savedupdateddeletingdeleted 模型事件。这是因为在执行批量更新或删除时,永远不会实际检索模型。

使用闭包

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

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
 
class User extends Model
{
/**
* The "booted" method of the model.
*/
protected static function booted(): void
{
static::created(function (User $user) {
// ...
});
}
}

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

use function Illuminate\Events\queueable;
 
static::created(queueable(function (User $user) {
// ...
}));

观察者

定义观察者

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

php artisan make:observer UserObserver --model=User

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

<?php
 
namespace App\Observers;
 
use App\Models\User;
 
class UserObserver
{
/**
* Handle the User "created" event.
*/
public function created(User $user): void
{
// ...
}
 
/**
* Handle the User "updated" event.
*/
public function updated(User $user): void
{
// ...
}
 
/**
* Handle the User "deleted" event.
*/
public function deleted(User $user): void
{
// ...
}
 
/**
* Handle the User "restored" event.
*/
public function restored(User $user): void
{
// ...
}
 
/**
* Handle the User "forceDeleted" event.
*/
public function forceDeleted(User $user): void
{
// ...
}
}

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

use App\Observers\UserObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
 
#[ObservedBy([UserObserver::class])]
class User extends Authenticatable
{
//
}

或者,您可以通过调用您希望观察的模型的 observe 方法来手动注册观察者。您可以在应用程序的 AppServiceProvider 类的 boot 方法中注册观察者。

use App\Models\User;
use App\Observers\UserObserver;
 
/**
* Bootstrap any application services.
*/
public function boot(): void
{
User::observe(UserObserver::class);
}
lightbulb

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

观察者和数据库事务

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

<?php
 
namespace App\Observers;
 
use App\Models\User;
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;
 
class UserObserver implements ShouldHandleEventsAfterCommit
{
/**
* Handle the User "created" event.
*/
public function created(User $user): void
{
// ...
}
}

静音事件

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

use App\Models\User;
 
$user = User::withoutEvents(function () {
User::findOrFail(1)->delete();
 
return User::find(2);
});

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

有时您可能希望“保存”给定模型而不分发任何事件。您可以使用 saveQuietly 方法来实现此目的。

$user = User::findOrFail(1);
 
$user->name = 'Victoria Faith';
 
$user->saveQuietly();

您还可以“更新”、“删除”、“软删除”、“恢复”和“复制”给定模型而不分发任何事件。

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