快速入门 本文未发布 发布文章
Eloquent:快速入门
简介
Laravel 内置 Eloquent 对象关系映射器(ORM),让数据库操作更加便捷。使用 Eloquent 时,每张数据库表都有一个对应的「模型」,用于操作该表。除了检索记录,Eloquent 模型也支持向表中插入、更新和删除记录。
[!NOTE]
开始之前,请先在应用的config/database.php配置文件中设置数据库连接。更多信息请参阅数据库配置文档。
生成模型类
首先创建一个 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
{
// ...
}
表名
上面的示例没有明确指定 Flight 模型对应的数据库表。按照约定,除非另行指定,Eloquent 会使用类名的复数形式,并转换为下划线命名法(snake case)作为表名。因此,Flight 模型对应 flights 表,而 AirTrafficController 模型对应 air_traffic_controllers 表。
如果模型对应的数据库表不符合此约定,可以通过 Table 特性手动指定表名:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;
#[Table('my_flights')]
class Flight extends Model
{
// ...
}
主键
Eloquent 还假定每个模型对应的数据库表都有一个名为 id 的主键列。你可以通过 Table 特性的 key 参数,指定其他列作为模型主键:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;
#[Table(key: 'flight_id')]
class Flight extends Model
{
// ...
}
此外,Eloquent 假定主键是自增整数,因此会自动将主键转换为整数。如果使用非自增或非数字主键,应在 Table 特性中指定 keyType 和 incrementing 参数:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;
#[Table(key: 'uuid', keyType: 'string', incrementing: false)]
class Flight extends Model
{
// ...
}
如果只需禁用自增 ID,可以使用 WithoutIncrementing 特性:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\WithoutIncrementing;
use Illuminate\Database\Eloquent\Model;
#[WithoutIncrementing]
class Flight extends Model
{
// ...
}
「复合」主键
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; // "018f2b5c-6a7f-7b12-9d6f-2f8a4e0c9c11"
默认情况下,HasUuids trait 会为模型生成 UUIDv7 标识符。这些 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。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_at 和 updated_at 列,并在创建或更新模型时自动设置它们的值。如果不希望 Eloquent 自动管理这些列,可以在模型的 Table 特性中将 timestamps 设为 false:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;
#[Table(timestamps: false)]
class Flight extends Model
{
// ...
}
如果只需禁用时间戳,可以使用 WithoutTimestamps 特性:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\WithoutTimestamps;
use Illuminate\Database\Eloquent\Model;
#[WithoutTimestamps]
class Flight extends Model
{
// ...
}
要自定义模型时间戳的格式,可以使用 Table 特性的 dateFormat 参数。它决定日期属性在数据库中的存储格式:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;
#[Table(dateFormat: 'U')]
class Flight extends Model
{
// ...
}
如果只需定义日期格式,可以使用 DateFormat 特性:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\DateFormat;
use Illuminate\Database\Eloquent\Model;
#[DateFormat('U')]
class Flight extends Model
{
// ...
}
要自定义存储时间戳的列名,可以在模型中定义 CREATED_AT 和 UPDATED_AT 常量:
<?php
class Flight extends Model
{
/**
* 存储创建时间的列名。
*
* @var string|null
*/
public const CREATED_AT = 'creation_date';
/**
* 存储更新时间的列名。
*
* @var string|null
*/
public const UPDATED_AT = 'updated_date';
}
要在执行模型操作时避免修改 updated_at 时间戳,可以将操作放在传给 withoutTimestamps 方法的闭包中:
Model::withoutTimestamps(fn () => $post->increment('reads'));
数据库连接
默认情况下,所有 Eloquent 模型都使用应用配置的默认数据库连接。要为某个模型指定其他连接,可以使用 Connection 特性:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Connection;
use Illuminate\Database\Eloquent\Model;
#[Connection('mysql')]
class Flight extends Model
{
// ...
}
默认属性值
默认情况下,新创建的模型实例不包含任何属性值。要为某些属性定义默认值,可以在模型中定义 $attributes 属性。该数组中的值应使用原始的「可存储」格式,如同刚从数据库中读取一样:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 模型属性的默认值。
*
* @var array<string, mixed>
*/
protected $attributes = [
'options' => '[]',
'delayed' => false,
];
}
写入后刷新属性
如果数据库中包含生成列,可以配置 Eloquent,在插入或更新模型后刷新指定属性。为此,请在模型上定义 Refreshes 特性:
use Illuminate\Database\Eloquent\Attributes\Refreshes;
#[Refreshes('name')]
class User extends Model
{
// ...
}
你可以通过数组指定多个属性:
#[Refreshes(['name', 'slug'])]
模型写入数据库后,指定属性会从数据库重新加载。
配置 Eloquent 严格模式
Laravel 提供了多种方法,用于配置 Eloquent 在不同场景下的行为和「严格程度」。
preventLazyLoading 方法接受一个可选的布尔参数,用来决定是否禁止延迟加载。例如,你可能只希望在非生产环境中禁用延迟加载,以免生产代码中意外使用了延迟加载关系时影响应用运行。通常应在应用 AppServiceProvider 的 boot 方法中调用该方法:
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 方法会返回模型对应表中的所有结果。由于每个模型本身也是查询构造器,你可以先添加查询约束,再调用 get 方法获取结果:
$flights = Flight::where('active', 1)
->orderBy('name')
->limit(10)
->get();
[!NOTE]
Eloquent 模型也是查询构造器,因此建议了解 Laravel 查询构造器提供的所有方法。编写 Eloquent 查询时,这些方法都可以使用。
刷新模型
对于已从数据库检索的模型实例,可以使用 fresh 和 refresh 方法「刷新」数据。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"
如果需要在事务中刷新模型并获取悲观锁,可以使用 refreshForUpdate 方法。它会使用 FOR UPDATE 锁重新加载模型:
DB::transaction(function () use ($flight) {
$flight->refreshForUpdate();
// 更新已加锁的模型……
});
集合
如前所述,all、get 等 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;
});
除了基础集合类的方法,Eloquent 集合类还提供了一些额外方法,专门用于处理 Eloquent 模型集合。
Laravel 的所有集合都实现了 PHP 的可迭代接口,因此可以像数组一样遍历:
foreach ($flights as $flight) {
echo $flight->name;
}
分块处理结果
如果使用 all 或 get 一次加载数万条 Eloquent 记录,应用可能耗尽内存。此时可以使用 chunk 方法,更高效地处理大量模型。
chunk 方法每次检索一部分 Eloquent 模型,并将它们传给闭包处理。由于每次只加载当前分块的模型,处理大量模型时可以显著减少内存占用:
use App\Models\Flight;
use Illuminate\Database\Eloquent\Collection;
Flight::chunk(200, function (Collection $flights) {
foreach ($flights as $flight) {
// ...
}
});
chunk 的第一个参数指定每个「分块」的记录数,第二个参数是处理各分块的闭包。每获取一个分块,都会执行一次数据库查询,再将结果传给闭包。
如果查询筛选条件涉及某一列,而你在遍历结果时也会更新该列,应使用 chunkById。这种情况下使用 chunk 可能导致意外或不一致的结果。chunkById 在内部始终查询 id 大于上一分块最后一个模型的记录:
Flight::where('departed', true)
->chunkById(200, function (Collection $flights) {
$flights->each->update(['departed' => false]);
}, column: 'id');
chunkById 和 lazyById 会自行向查询添加 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 方法类似,内部也会分块执行查询。但它不会将各分块直接传给回调,而是返回一个已展平的 Eloquent 模型惰性集合(LazyCollection),让你将结果作为连续的数据流处理:
use App\Models\Flight;
foreach (Flight::lazy() as $flight) {
// ...
}
如果 lazy 的查询筛选条件涉及某一列,而你在遍历结果时也会更新该列,应使用 lazyById。它在内部始终检索 id 大于上一分块最后一个模型的记录:
Flight::where('departed', true)
->lazyById(200, column: 'id')
->each->update(['departed' => false]);
你也可以使用 lazyByIdDesc,按 id 降序检索结果。
游标
与 lazy 类似,cursor 方法可以在遍历数万条 Eloquent 模型记录时显著降低内存消耗。
cursor 只执行一次数据库查询,但只有遍历到某条记录时才会将其填充为 Eloquent 模型。因此,遍历游标时,内存中始终只保留一个模型。
[!WARNING]
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 每次只保留一个模型,比普通查询节省许多内存,但数据量足够大时仍会耗尽内存。这是因为 PHP 的 PDO 驱动会在内部缓冲区缓存所有原始查询结果。如果需要处理海量 Eloquent 记录,请考虑使用 lazy 方法。
高级子查询
子查询选择
Eloquent 支持高级子查询,可在一次查询中获取关联表的信息。例如,假设有一张航班目的地表 destinations 和一张飞往各目的地的航班表 flights。flights 表中的 arrived_at 列记录航班到达目的地的时间。
通过查询构造器 select 和 addSelect 方法的子查询功能,可以用一次查询获取所有目的地,以及最近到达各目的地的航班名称:
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();
检索单个模型与聚合结果
除了检索查询匹配的所有记录,也可以使用 find、first 或 firstWhere 获取单条记录。这些方法返回单个模型实例:
use App\Models\Flight;
// 根据主键检索模型……
$flight = Flight::find(1);
// 检索符合查询条件的第一个模型……
$flight = Flight::where('active', 1)->first();
// 检索符合查询条件的第一个模型的另一种写法……
$flight = Flight::firstWhere('active', 1);
有时需要在未找到记录时执行其他操作。findOr 和 firstOr 会在找到记录时返回模型实例,否则执行给定闭包,并将闭包的返回值作为方法结果:
$flight = Flight::findOr(1, function () {
// ...
});
$flight = Flight::where('legs', '>', 3)->firstOr(function () {
// ...
});
未找到模型时抛出异常
你也可以在未找到模型时抛出异常,这在路由或控制器中特别有用。findOrFail 和 firstOrFail 会检索查询的第一个结果;如果没有找到,则抛出 Illuminate\Database\Eloquent\ModelNotFoundException:
$flight = Flight::findOrFail(1);
$flight = Flight::where('legs', '>', 3)->firstOrFail();
如果没有捕获 ModelNotFoundException,Laravel 会自动向客户端返回 HTTP 404 响应:
use App\Models\Flight;
Route::get('/api/flights/{id}', function (string $id) {
return Flight::findOrFail($id);
});
检索或创建模型
firstOrCreate 会根据给定的列和值查找数据库记录。如果找不到,就合并第一个数组参数与可选的第二个数组参数,并使用合并后的属性插入记录。
firstOrNew 与 firstOrCreate 类似,也会根据给定属性查找记录。但找不到时,它只返回一个新模型实例,不会写入数据库。你需要手动调用 save 保存该实例:
use App\Models\Flight;
// 根据名称检索航班,不存在时创建……
$flight = Flight::firstOrCreate([
'name' => 'London to Paris'
]);
// 根据名称检索航班,不存在时使用 name、delayed 和 arrival_time 属性创建……
$flight = Flight::firstOrCreate(
['name' => 'London to Paris'],
['delayed' => 1, 'arrival_time' => '11:30']
);
// 根据名称检索航班,不存在时实例化一个新的 Flight……
$flight = Flight::firstOrNew([
'name' => 'London to Paris'
]);
// 根据名称检索航班,不存在时使用 name、delayed 和 arrival_time 属性实例化……
$flight = Flight::firstOrNew(
['name' => 'Tokyo to Sydney'],
['delayed' => 1, 'arrival_time' => '11:30']
);
检索聚合结果
使用 Eloquent 模型时,也可以调用 Laravel 查询构造器提供的 count、sum、max 等聚合方法。这些方法返回标量值,而非模型实例:
$count = Flight::where('active', 1)->count();
$max = Flight::where('active', 1)->max('price');
插入和更新模型
插入
Eloquent 也支持向数据库插入记录。只需创建一个模型实例、设置其属性,再调用实例的 save 方法:
<?php
namespace App\Http\Controllers;
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 后,会向数据库插入记录,同时自动设置 created_at 和 updated_at 时间戳,无需手动赋值。
要在数据库事务中保存模型,可以使用 saveOrFail。如果保存过程中抛出异常,事务会自动回滚:
$flight->saveOrFail();
也可以使用 create,通过一条 PHP 语句保存新模型。该方法会返回已插入的模型实例:
use App\Models\Flight;
$flight = Flight::create([
'name' => 'London to Paris',
]);
不过,使用 create 前,需要在模型类上指定 Fillable 或 Guarded 特性,因为所有 Eloquent 模型默认都启用了批量赋值保护。详情请参阅批量赋值文档。
更新
save 也可以更新数据库中的现有模型。先检索模型并修改所需属性,再调用 save。updated_at 时间戳同样会自动更新,无需手动设置:
use App\Models\Flight;
$flight = Flight::find(1);
$flight->name = 'Paris to London';
$flight->save();
要在数据库事务中更新模型,可以使用 updateOrFail。如果更新过程中抛出异常,事务会自动回滚:
$flight->updateOrFail(['name' => 'Paris to London']);
有时需要更新现有模型,并在没有匹配模型时创建新模型。可以使用 updateOrCreate。它与 firstOrCreate 一样会直接保存模型,无需手动调用 save。
以下示例会查找 departure 为 Oakland、destination 为 San Diego 的航班。如果存在,就更新其 price 和 discounted 列;否则,合并两个参数数组中的属性,创建一个新航班:
$flight = Flight::updateOrCreate(
['departure' => 'Oakland', 'destination' => 'San Diego'],
['price' => 99, 'discounted' => 1]
);
使用 firstOrCreate 或 updateOrCreate 等方法时,你可能需要区分模型是新创建的,还是已存在的。wasRecentlyCreated 属性表示模型是否在当前生命周期中创建:
$flight = Flight::updateOrCreate(
// ...
);
if ($flight->wasRecentlyCreated) {
// 已插入新的航班记录……
}
批量更新
你也可以更新查询所匹配的所有模型。以下示例将所有处于活动状态、且目的地为 San Diego 的航班标记为延误:
Flight::where('active', 1)
->where('destination', 'San Diego')
->update(['delayed' => 1]);
update 接受一个由列名和值组成的数组,指定需要更新的列,并返回受影响的行数。
[!WARNING]
通过 Eloquent 批量更新时,不会为受影响的模型触发saving、saved、updating和updated事件,因为批量更新并未实际检索这些模型。
检查属性变化
Eloquent 提供 isDirty、isClean 和 wasChanged 方法,用于检查模型内部状态,了解属性自检索以来的变化。
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(); // 原始属性数组……
getChanges 返回模型最近一次保存时发生变化的属性数组,getPrevious 则返回最近一次保存前的原始属性值数组:
$user = User::find(1);
$user->name; // John
$user->email; // john@example.com
$user->update([
'name' => 'Jack',
'email' => 'jack@example.com',
]);
$user->getChanges();
/*
[
'name' => 'Jack',
'email' => 'jack@example.com',
]
*/
$user->getPrevious();
/*
[
'name' => 'John',
'email' => 'john@example.com',
]
*/
批量赋值
你可以使用 create,通过一条 PHP 语句保存新模型。该方法返回已插入的模型实例:
use App\Models\Flight;
$flight = Flight::create([
'name' => 'London to Paris',
]);
不过,使用 create 前,需要在模型类上指定 Fillable 或 Guarded 特性,因为所有 Eloquent 模型默认都启用了批量赋值保护。
如果用户在 HTTP 请求中传入了非预期字段,而该字段修改了本不应允许用户修改的数据库列,就会发生批量赋值漏洞。例如,恶意用户可能在请求中发送 is_admin 参数。如果该参数直接传给模型的 create 方法,就可能让用户将自己提升为管理员。
因此,首先应通过模型的 Fillable 特性,明确哪些属性允许批量赋值。例如,将 Flight 模型的 name 属性设为可批量赋值:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['name'])]
class Flight extends Model
{
// ...
}
指定可批量赋值的属性后,就可以使用 create 向数据库插入记录,并获取新创建的模型实例:
$flight = Flight::create(['name' => 'London to Paris']);
如果已有模型实例,可以使用 fill 将属性数组填充到该实例中:
$flight->fill(['name' => 'Amsterdam to Frankfurt']);
批量赋值与 JSON 列
对 JSON 列批量赋值时,必须在模型的 Fillable 特性中明确列出允许赋值的键。出于安全考虑,使用 Guarded 特性时,Laravel 不支持更新嵌套 JSON 属性:
use Illuminate\Database\Eloquent\Attributes\Fillable;
#[Fillable(['options->enabled'])]
class Flight extends Model
{
// ...
}
允许所有属性批量赋值
要允许所有属性批量赋值,可以在模型上使用 Unguarded 特性。解除保护后,应格外谨慎,始终手动构建传给 Eloquent 的 fill、create 和 update 方法的数组:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Unguarded;
use Illuminate\Database\Eloquent\Model;
#[Unguarded]
class Flight extends Model
{
// ...
}
批量赋值异常
默认情况下,批量赋值时未包含在 Fillable 特性中的属性会被静默丢弃。这在生产环境中是预期行为,但本地开发时,可能让你难以察觉模型修改未生效的原因。
你可以调用 preventSilentlyDiscardingAttributes,让 Laravel 在尝试填充不可批量赋值的属性时抛出异常。通常应在应用 AppServiceProvider 的 boot 方法中调用:
use Illuminate\Database\Eloquent\Model;
/**
* 启动应用服务。
*/
public function boot(): void
{
Model::preventSilentlyDiscardingAttributes($this->app->isLocal());
}
更新或插入
Eloquent 的 upsert 方法可以在一次原子操作中更新或创建记录。第一个参数包含要插入或更新的值;第二个参数列出表中用于唯一标识记录的列;第三个参数指定已存在匹配记录时需要更新的列。如果模型启用了时间戳,upsert 会自动设置 created_at 和 updated_at:
Flight::upsert([
['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99],
['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150]
], uniqueBy: ['departure', 'destination'], update: ['price']);
[!WARNING]
除 SQL Server 外,所有数据库都要求upsert第二个参数指定的列具有主键或唯一索引。此外,MariaDB 和 MySQL 驱动会忽略第二个参数,始终使用表中已有的主键和唯一索引来判断记录是否存在。
删除模型
要删除模型,可以在模型实例上调用 delete:
use App\Models\Flight;
$flight = Flight::find(1);
$flight->delete();
要在数据库事务中删除模型,可以使用 deleteOrFail。如果删除过程中抛出异常,事务会自动回滚:
$flight->deleteOrFail();
根据主键删除现有模型
上面的示例先检索模型,再调用 delete。如果已知主键,可以直接调用 destroy,无需显式检索模型。该方法接受单个或多个主键,也接受主键数组或主键集合:
Flight::destroy(1);
Flight::destroy(1, 2, 3);
Flight::destroy([1, 2, 3]);
Flight::destroy(collect([1, 2, 3]));
对于使用软删除的模型,可以通过 forceDestroy 永久删除:
Flight::forceDestroy(1);
[!WARNING]
destroy会逐个加载模型并调用delete,以便为每个模型正确派发deleting和deleted事件。
通过查询删除模型
你也可以构建 Eloquent 查询,删除所有匹配的模型。以下示例删除所有标记为非活动的航班。与批量更新一样,批量删除不会为被删除的模型派发模型事件:
$deleted = Flight::where('active', 0)->delete();
要删除表中的所有模型,请执行不带任何条件的删除查询:
$deleted = Flight::query()->delete();
[!WARNING]
通过 Eloquent 批量删除时,不会为被删除的模型派发deleting和deleted事件,因为执行删除语句时并未实际检索这些模型。
软删除
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;
}
[!NOTE]
SoftDeletestrait 会自动将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。该方法会将模型的 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\Prunable 或 Illuminate\Database\Eloquent\MassPrunable 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;
/**
* 获取待修剪模型的查询。
*/
public function prunable(): Builder
{
return static::where('created_at', '<=', now()->minus(months: 1));
}
}
使用 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();
可以给 model:prune 加上 --pretend 选项来检查 prunable 查询。此时命令只报告实际运行时会修剪的记录数:
php artisan model:prune --pretend
[!WARNING]
软删除的模型如果匹配修剪查询,将被永久删除(forceDelete)。
批量修剪
使用 Illuminate\Database\Eloquent\MassPrunable trait 时,会通过批量删除查询移除模型。因此不会调用 pruning 方法,也不会派发 deleting 和 deleted 事件,因为删除前不会实际检索这些模型。这种方式能显著提高修剪效率:
<?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()->minus(months: 1));
}
}
复制模型
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 接口的类。该接口要求实现 apply 方法,你可以在其中向查询添加 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()->minus(years: 2000));
}
}
[!NOTE]
如果全局作用域需要向查询的 select 子句添加列,应使用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 手动注册全局作用域。该方法只接受一个作用域实例参数:
<?php
namespace App\Models;
use App\Models\Scopes\AncientScope;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 模型的 booted 方法。
*/
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
{
/**
* 模型的 booted 方法。
*/
protected static function booted(): void
{
static::addGlobalScope('ancient', function (Builder $builder) {
$builder->where('created_at', '<', now()->minus(years: 2000));
});
}
}
移除全局作用域
要在某次查询中移除全局作用域,可以调用 withoutGlobalScope,并传入全局作用域的类名:
User::withoutGlobalScope(AncientScope::class)->get();
如果作用域是通过闭包定义的,则传入为该作用域指定的名称:
User::withoutGlobalScope('ancient')->get();
要移除多个或全部全局作用域,可以使用 withoutGlobalScopes 和 withoutGlobalScopesExcept:
// 移除所有全局作用域……
User::withoutGlobalScopes()->get();
// 移除部分全局作用域……
User::withoutGlobalScopes([
FirstScope::class, SecondScope::class
])->get();
// 移除指定作用域以外的所有全局作用域……
User::withoutGlobalScopesExcept([
SecondScope::class,
])->get();
局部作用域
局部作用域可以将常用的查询约束定义在一起,方便在应用中复用。例如,你可能经常需要检索所有「热门」用户。要定义作用域,请在 Eloquent 方法上添加 Scope 特性。
作用域方法应始终返回同一个查询构造器实例或 void:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 将查询限定为仅包含热门用户。
*/
#[Scope]
protected function popular(Builder $query): void
{
$query->where('votes', '>', 100);
}
/**
* 将查询限定为仅包含活跃用户。
*/
#[Scope]
protected function active(Builder $query): void
{
$query->where('active', 1);
}
}
使用局部作用域
定义作用域后,就可以在查询模型时调用作用域方法,也可以链式调用多个作用域:
use App\Models\User;
$users = User::popular()->active()->orderBy('created_at')->get();
通过 or 运算符组合多个作用域时,可能需要使用闭包,确保正确的逻辑分组:
$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\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 将查询限定为仅包含指定类型的用户。
*/
#[Scope]
protected function ofType(Builder $query, string $type): void
{
$query->where('type', $type);
}
}
在方法签名中添加参数后,就可以在调用作用域时传值:
$users = User::ofType('admin')->get();
使用特性声明的作用域方法应为 protected。在模型类内部调用这类作用域时,应通过查询构造器实例调用,例如 static::query()->ofType('admin'),确保由 Eloquent 的作用域机制处理调用。
待处理属性
如果希望通过作用域创建模型,并让模型具有该作用域用于筛选的属性值,可以在构建作用域查询时使用 withAttributes:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* 将查询限定为仅包含草稿。
*/
#[Scope]
protected function draft(Builder $query): void
{
$query->withAttributes([
'hidden' => true,
]);
}
}
withAttributes 会根据给定属性添加 where 条件,并将这些属性应用到通过该作用域创建的模型:
$draft = Post::draft()->create(['title' => 'In Progress']);
$draft->hidden; // true
如果不希望 withAttributes 添加 where 条件,可以将 asConditions 参数设为 false:
$query->withAttributes([
'hidden' => true,
], asConditions: false);
比较模型
可以使用 is 和 isNot,快速判断两个模型是否具有相同的主键、表和数据库连接:
if ($post->is($anotherPost)) {
// ...
}
if ($post->isNot($anotherPost)) {
// ...
}
belongsTo、hasOne、morphTo 和 morphOne 关系也提供了 is 和 isNot,可直接比较关联模型,无需先执行查询检索模型:
if ($post->author()->is($user)) {
// ...
}
事件
[!NOTE]
要将 Eloquent 事件直接广播到客户端应用,请参阅 Laravel 的模型事件广播。
Eloquent 模型会派发多种事件,方便你在模型生命周期的各个阶段执行操作,包括:retrieved、creating、created、updating、updated、saving、saved、deleting、deleted、trashed、forceDeleting、forceDeleted、restoring、restored 和 replicating。
从数据库检索现有模型时,会派发 retrieved。首次保存新模型时,会派发 creating 和 created。修改现有模型并调用 save 时,会派发 updating / updated。创建或更新模型时,即使属性没有变化,也会派发 saving / saved。以 -ing 结尾的事件在模型变更写入数据库之前派发,以 -ed 结尾的事件则在写入之后派发。
要监听模型事件,可以在 Eloquent 模型中定义 $dispatchesEvents 属性,将生命周期中的各个事件映射到你自己的事件类。每个事件类都应通过构造函数接收受影响的模型实例:
<?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,
];
}
定义事件映射后,就可以使用事件监听器处理这些事件。
[!WARNING]
通过 Eloquent 批量更新或删除时,不会为受影响的模型派发saved、updated、deleting和deleted事件,因为这些操作没有实际检索模型。
使用闭包
除了自定义事件类,你也可以注册闭包,在模型事件派发时执行。通常应在模型的 booted 方法中注册:
<?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 通过应用的队列在后台执行监听器:
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
{
/**
* 处理 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 特性:
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;
/**
* 启动应用服务。
*/
public function boot(): void
{
User::observe(UserObserver::class);
}
[!NOTE]
观察者还可以监听saving、retrieved等其他事件,详情请参阅事件部分。
观察者与数据库事务
在数据库事务中创建模型时,你可能希望观察者等事务提交后再处理事件。为此,可以让观察者实现 ShouldHandleEventsAfterCommit 接口。如果当前没有进行中的数据库事务,事件处理方法会立即执行:
<?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 返回:
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();
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
Laravel 13 中文文档
关于 LearnKu
推荐文章: