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 FlightEloquent 模型约定
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\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 假设主键是自增整数值,这意味着 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
{
// ...
}「复合」主键
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」而不是 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_at 和 updated_at 列。当创建或更新模型时,Eloquent 将自动设置这些列的值。如果您不希望这些列由 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
{
// ...
}如果您需要自定义模型时间戳的格式,可以使用 Table 属性的 dateFormat 参数。这决定了日期属性如何存储在数据库中以及模型序列化为数组或 JSON 时的格式:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;
#[Table(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 属性。放置在 $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 方法接受一个可选的布尔参数,指示是否应防止延迟加载。例如,您可能希望仅在非生产环境中禁用延迟加载,以便即使生产代码中意外存在延迟加载的关系,您的生产环境也能继续正常运行。通常,此方法应在应用程序 AppServiceProvider 的 boot 方法中调用:
use Illuminate\Database\Eloquent\Model;
/**
* 引导任何应用程序服务。
*/
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}此外,您可以指示 Laravel 在尝试填充不可填充属性时抛出异常,方法是调用 preventSilentlyDiscardingAttributes 方法。这有助于在本地开发期间防止意外错误,当尝试设置尚未添加到模型 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')
->limit(10)
->get();NOTE
由于 Eloquent 模型是查询构建器,您应该查看 Laravel 查询构建器提供的所有方法。在编写 Eloquent 查询时,您可以使用这些方法中的任何一个。
刷新模型
如果您已经有一个从数据库检索的 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"集合
正如我们所见,Eloquent 的 all 和 get 等方法从数据库检索多条记录。但是,这些方法不返回普通的 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;
}分块结果
如果您尝试通过 all 或 get 方法加载数万个 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');由于 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 方法,因为它在幕后分块执行查询。但是,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 模型。
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 方法使用的内存比常规查询少得多(一次只在内存中保存单个 Eloquent 模型),但它最终仍会耗尽内存。这是因为 PHP 的 PDO 驱动程序在内部将其缓冲区中的所有原始查询结果缓存。如果您要处理大量 Eloquent 记录,请考虑使用 lazy 方法。
高级子查询
子查询选择
Eloquent 还提供高级子查询支持,允许您在单个查询中从相关表中提取信息。例如,假设我们有一个航班 destinations 表和一个到目的地的 flights 表。flights 表包含一个 arrived_at 列,指示航班何时到达目的地。
使用查询构建器的 select 和 addSelect 方法可用的子查询功能,我们可以选择所有 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();检索单个模型 / 聚合
除了检索匹配给定查询的所有记录外,您还可以使用 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,将自动向客户端发送 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'
]);
// 按名称检索航班,或使用名称、delayed 和 arrival_time 属性创建它...
$flight = Flight::firstOrCreate(
['name' => 'London to Paris'],
['delayed' => 1, 'arrival_time' => '11:30']
);
// 按名称检索航班或实例化新的 Flight 实例...
$flight = Flight::firstOrNew([
'name' => 'London to Paris'
]);
// 按名称检索航班或使用名称、delayed 和 arrival_time 属性实例化...
$flight = Flight::firstOrNew(
['name' => 'Tokyo to Sydney'],
['delayed' => 1, 'arrival_time' => '11:30']
);检索聚合
与 Eloquent 模型交互时,您还可以使用 count、sum、max 和 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\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_at 和 updated_at 时间戳将自动设置,因此无需手动设置它们。
或者,您可以使用 create 方法使用单个 PHP 语句「保存」新模型。create 方法将返回插入的模型实例:
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();有时,您可能需要更新现有模型,或者如果没有匹配的模型则创建新模型。与 firstOrCreate 方法一样,updateOrCreate 方法持久化模型,因此无需手动调用 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) {
// 插入了新航班记录...
}批量更新
也可以对匹配给定查询的模型执行更新。在此示例中,所有 active 且 destination 为 San Diego 的航班将被标记为延迟:
Flight::where('active', 1)
->where('destination', 'San Diego')
->update(['delayed' => 1]);update 方法期望一个表示应更新的列的列和值对数组。update 方法返回受影响的行数。
WARNING
通过 Eloquent 发出批量更新时,不会为更新的模型触发 saving、saved、updating 和 updated 模型事件。这是因为发出批量更新时从未实际检索模型。
检查属性更改
Eloquent 提供 isDirty、isClean 和 wasChanged 方法来检查模型的内部状态,并确定其属性自最初检索模型以来如何更改。
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(); // truewasChanged 方法确定在当前请求周期中上次保存模型时是否更改了任何属性。如有必要,您可以传递属性名称以查看特定属性是否已更改:
$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']); // truegetOriginal 方法返回一个包含模型原始属性的数组,无论自检索以来模型有何更改。如有必要,您可以传递特定属性名称以获取特定属性的原始值:
$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 请求字段,而该字段更改了您未预期的数据库列时,就会发生批量赋值漏洞。例如,恶意用户可能通过 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 方法在数据库中插入新记录。create 方法返回新创建的模型实例:
$flight = Flight::create(['name' => 'London to Paris']);如果您已经有一个模型实例,可以使用 fill 方法用属性数组填充它:
$flight->fill(['name' => 'Amsterdam to Frankfurt']);批量赋值和 JSON 列
分配 JSON 列时,每个列的可批量赋值键必须在模型的 Fillable 属性中指定。出于安全考虑,Laravel 不支持在使用 Guarded 属性时更新嵌套 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 属性中的属性将被静默丢弃。在生产中,这是预期行为;但是,在本地开发期间,这可能会导致混淆,因为不清楚为什么模型更改未生效。
如果您愿意,可以指示 Laravel 在尝试填充不可填充属性时抛出异常,方法是调用 preventSilentlyDiscardingAttributes 方法。通常,此方法应在应用程序 AppServiceProvider 类的 boot 方法中调用:
use Illuminate\Database\Eloquent\Model;
/**
* 引导任何应用程序服务。
*/
public function boot(): void
{
Model::preventSilentlyDiscardingAttributes($this->app->isLocal());
}Upserts
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 数据库驱动程序忽略 upsert 方法的第二个参数,并始终使用表的「主」和「唯一」索引来检测现有记录。
删除模型
要删除模型,可以在模型实例上调用 delete 方法:
use App\Models\Flight;
$flight = Flight::find(1);
$flight->delete();通过主键删除现有模型
在上面的示例中,我们在调用 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);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
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\Prunable 或 Illuminate\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;
/**
* 获取可修剪的模型查询。
*/
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 目录中的「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 --pretendWARNING
如果软删除模型匹配可修剪查询,它们将被永久删除(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 接口的类。Scope 接口要求您实现一个方法:apply。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。这将防止无意中替换查询的现有 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
{
/**
* 模型的「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();局部作用域
局部作用域允许您定义可在整个应用程序中轻松重用的常见查询约束集。例如,您可能需要频繁检索所有被认为是「受欢迎」的用户。要定义作用域,将 Scope 属性添加到 Eloquent 方法。
作用域应始终返回相同的查询构建器实例或 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 查询运算符组合多个 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\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();待定属性
如果您希望使用作用域创建具有与用于约束作用域的相同属性的模型,可以在构建作用域查询时使用 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 属性。此属性将 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 发出批量更新或删除查询时,不会为受影响的模型调度 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 方法接受闭包作为其唯一参数。在此闭包内执行的任何代码都不会调度模型事件,闭包返回的任何值将由 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();