Appearance
Eloquent入门
介绍
Laravel 包括 Eloquent,这是一种对象关系映射器 (ORM),可让您与数据库进行交互。使用 Eloquent 时,每个数据库表都有一个相应的“模型”,用于与该表交互。除了从数据库表中检索记录外,Eloquent 模型还允许您从表中插入、更新和删除记录。
NOTE
在开始之前,请务必在应用程序的 config/database.php
配置文件中配置数据库连接。有关配置数据库的更多信息,请查看数据库 配置文档。
Laravel 训练营
如果您是 Laravel 的新手,请随时加入 Laravel 训练营。Laravel 训练营将引导您使用 Eloquent 构建您的第一个 Laravel 应用程序。这是了解 Laravel 和 Eloquent 所提供的一切的好方法。
生成模型类
首先,让我们创建一个 Eloquent 模型。模型通常位于 app\Models
目录中,并扩展类 Illuminate\Database\Eloquent\Model
。您可以使用 make:model
Artisan 命令生成新模型:
shell
php artisan make:model Flight
如果您想在生成模型时生成数据库迁移,您可以使用 --migration
或 -m
选项:
shell
php artisan make:model Flight --migration
在生成模型时,您可以生成各种其他类型的类,例如 factories, seeders, policies, controllers, 和表单请求。此外,还可以组合这些选项以一次创建多个类:
shell
# 生成一个模型和一个 FlightFactory 类...
php artisan make:model Flight --factory
php artisan make:model Flight -f
# 生成一个模型和一个 FlightSeeder 类...
php artisan make:model Flight --seed
php artisan make:model Flight -s
# 生成一个模型和一个 FlightController 类...
php artisan make:model Flight --controller
php artisan make:model Flight -c
# 生成模型、FlightController 资源类和表单请求类...
php artisan make:model Flight --controller --resource --requests
php artisan make:model Flight -crR
# 生成一个模型和一个 FlightPolicy 类...
php artisan make:model Flight --policy
# 生成 model and a migration, factory, seeder, and controller...
php artisan make:model Flight -mfsc
# 生成模型、迁移、工厂、种子机、策略、控制器和表单请求的快捷方式...
php artisan make:model Flight --all
php artisan make:model Flight -a
# 生成一个 pivot 模型...
php artisan make:model Member --pivot
php artisan make:model Member -p
检查模型
有时,仅通过浏览其代码可能很难确定模型的所有可用属性和关系。相反,请尝试 model:show
Artisan 命令,该命令提供了模型所有属性和关系的便捷概览:
shell
php artisan model:show Flight
Eloquent 模型约定
由 make:model
命令生成的模型将被放置在 app/Models
目录中。让我们看一个基本的模型类,并讨论 Eloquent 的一些关键约定:
php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
// ...
}
表名
看了一眼上面的例子后,你可能已经注意到我们没有告诉 Eloquent 哪个数据库表对应于我们的 Flight
模型。按照惯例,除非明确指定了其他名称,否则类的 “snake case” 复数名称将用作表名。因此,在这种情况下,Eloquent 将假设 Flight
模型将记录存储在 flights
表中,而 AirTrafficController
模型将记录存储在 air_traffic_controllers
表中。
如果你的模型对应的 database table 不符合这个约定,你可以通过在模型上定义 table
属性来手动指定模型的 table name:
php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 与模型关联的表
*
* @var string
*/
protected $table = 'my_flights';
}
主键
Eloquent 还将假设每个模型的相应数据库表都有一个名为 id
的主键列。如有必要,您可以在模型上定义受保护的 $primaryKey
属性,以指定用作模型主键的不同列:
php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 与表关联的主键
*
* @var string
*/
protected $primaryKey = 'flight_id';
}
此外,Eloquent 假设主键是一个递增的整数值,这意味着 Eloquent 会自动将主键转换为整数。如果你想使用非递增或非数字主键,你必须在模型上定义一个设置为 false
的 public $incrementing
属性:
php
<?php
class Flight extends Model
{
/**
* 指示模型的 ID 是否为自动递增
*
* @var bool
*/
public $incrementing = false;
}
如果模型的主键不是整数,则应在模型上定义受保护的 $keyType
属性。此属性的值应为 string
:
php
<?php
class Flight extends Model
{
/**
* 主键 ID 的数据类型
*
* @var string
*/
protected $keyType = 'string';
}
“复合” 主键
Eloquent 要求每个模型至少有一个唯一标识的 “ID” 作为其主键。Eloquent 模型不支持 “复合” 主键。但是,除了表的唯一标识主键之外,您还可以自由地向数据库表添加其他多列唯一索引。
UUID and ULID 键
您可以选择使用 UUID 而不是使用自动递增的整数作为 Eloquent 模型的主键。UUID 是通用唯一的字母数字标识符,长度为 36 个字符。
如果您希望模型使用 UUID 键而不是自动递增的整数键,则可以在模型上使用 Illuminate\Database\Eloquent\Concerns\HasUuids
trait。当然,你应该确保模型有一个 UUID 等效主键列:
php
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
use HasUuids;
// ...
}
$article = Article::create(['title' => 'Traveling to Europe']);
$article->id; // "8f8e8478-9035-4d23-b9a7-62f4d2612ce5"
默认情况下,HasUuids
特征将为您的模型生成“有序”的 UUID。这些 UUID 对于索引数据库存储更有效,因为它们可以按字典顺序排序。
您可以通过在模型上定义 newUniqueId
方法来覆盖给定模型的 UUID 生成过程。此外,您可以通过在模型上定义 uniqueIds
方法来指定哪些列应该接收 UUID:
php
use Ramsey\Uuid\Uuid;
/**
* 为模型生成新的 UUID
*/
public function newUniqueId(): string
{
return (string) Uuid::uuid4();
}
/**
* 获取应接收唯一标识符的列
*
* @return array<int, string>
*/
public function uniqueIds(): array
{
return ['id', 'discount_code'];
}
如果您愿意,您可以选择使用 “ULID” 而不是 UUID。ULID 类似于 UUID;但是,它们的长度只有 26 个字符。与有序 UUID 一样,ULID 是可按字典顺序排序的,以实现高效的数据库索引。要使用 ULID,您应该在模型上使用 Illuminate\Database\Eloquent\Concerns\HasUlids
trait。您还应确保模型具有 ULID 等效主键列:
php
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
use HasUlids;
// ...
}
$article = Article::create(['title' => 'Traveling to Asia']);
$article->id; // "01gd4d3tgrrfqeda94gdbtdk5c"
时间戳
默认情况下,Eloquent 期望 created_at
和 updated_at
列存在于模型的相应数据库表中。Eloquent 将在创建或更新模型时自动设置这些列的值。如果您不希望 Eloquent 自动管理这些列,则应在模型上定义一个值为 false
的 $timestamps
属性:
php
<?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
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 模型日期列的存储格式
*
* @var string
*/
protected $dateFormat = 'U';
}
如果需要自定义用于存储时间戳的列的名称,可以在模型上定义 CREATED_AT
和 UPDATED_AT
常量:
php
<?php
class Flight extends Model
{
const CREATED_AT = 'creation_date';
const UPDATED_AT = 'updated_date';
}
如果你想在不修改模型updated_at
时间戳的情况下执行模型操作,你可以在给定给 withoutTimestamps
方法的闭包中对模型进行操作:
php
Model::withoutTimestamps(fn () => $post->increment('reads'));
数据库连接
默认情况下,所有 Eloquent 模型都将使用为您的应用程序配置的默认数据库连接。如果要指定在与特定模型交互时应使用的不同连接,则应在模型上定义 $connection
属性:
php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* The database connection that should be used by the model.
*
* @var string
*/
protected $connection = 'mysql';
}
默认属性值
默认情况下,新实例化的模型实例将不包含任何属性值。如果您想为模型的某些属性定义默认值,您可以在模型上定义 $attributes
属性。放置在 $attributes
数组中的属性值应采用其原始的 “可存储” 格式,就像它们只是从数据库中读取一样:
php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 模型的属性默认值
*
* @var array
*/
protected $attributes = [
'options' => '[]',
'delayed' => false,
];
}
配置 Eloquent 严格性
Laravel 提供了多种方法,允许您在各种情况下配置 Eloquent 的行为和“严格性”。
首先,preventLazyLoading
方法接受一个可选的布尔参数,该参数指示是否应防止延迟加载。例如,您可能希望仅在非生产环境中禁用延迟加载,以便即使生产代码中意外存在延迟加载关系,您的生产环境也将继续正常运行。通常,应在应用程序的 AppServiceProvider
的 boot
方法中调用此方法:
php
use Illuminate\Database\Eloquent\Model;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}
此外,您可以指示 Laravel 在尝试通过调用 preventSilentlyDiscardingAttributes
该方法填充 unfillable 属性时抛出异常。这有助于防止在本地开发期间尝试设置尚未添加到模型的 fillable
数组的属性时出现意外错误:
php
Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());
检索模型
创建模型及其关联的数据库表后,您就可以开始从数据库中检索数据了。您可以将每个 Eloquent 模型视为一个强大的查询构建器,允许您流畅地查询与模型关联的数据库表。模型的 all
方法将从模型的关联数据库表中检索所有记录:
php
use App\Models\Flight;
foreach (Flight::all() as $flight) {
echo $flight->name;
}
构建查询
Eloquent all
方法将返回模型表中的所有结果。但是,由于每个 Eloquent 模型都用作查询构建器,因此您可以向查询添加其他约束,然后调用 get
方法来检索结果:
php
$flights = Flight::where('active', 1)
->orderBy('name')
->take(10)
->get();
NOTE
由于 Eloquent 模型是查询构建器,因此您应该查看 Laravel 的查询构建器提供的所有方法。在编写 Eloquent 查询时,您可以使用这些方法中的任何一种。
刷新模型
如果您已经有一个从数据库中检索到的 Eloquent 模型的实例,则可以使用 fresh
和 refresh
方法“刷新”模型。new
方法将从数据库中重新检索模型。现有模型实例不会受到影响:
php
$flight = Flight::where('number', 'FR 900')->first();
$freshFlight = $flight->fresh();
refresh
方法将使用数据库中的新数据重新冻结现有模型。此外,其所有加载的关系也将刷新:
php
$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
方法可用于根据调用的闭包的结果从集合中删除模型:
php
$flights = Flight::where('destination', 'Paris')->get();
$flights = $flights->reject(function (Flight $flight) {
return $flight->cancelled;
});
除了 Laravel 的基集合类提供的方法外,Eloquent 集合类还提供了一些专门用于与 Eloquent 模型集合交互的 额外方法。
由于 Laravel 的所有集合都实现了 PHP 的可迭代接口,因此您可以像数组一样循环遍历集合:
php
foreach ($flights as $flight) {
echo $flight->name;
}
分块结果
如果您尝试通过 all
或 get
方法加载数以万计的 Eloquent 记录,您的应用程序可能会耗尽内存。chunk 方法可以更有效地
处理大量模型,而不是使用这些方法。
chunk
方法将检索 Eloquent 模型的子集,将它们传递给闭包进行处理。由于一次只检索 Eloquent 模型的当前块,因此在处理大量模型时,chunk
方法将显着减少内存使用:
php
use App\Models\Flight;
use Illuminate\Database\Eloquent\Collection;
Flight::chunk(200, function (Collection $flights) {
foreach ($flights as $flight) {
// ...
}
});
传递给 chunk
方法的第一个参数是您希望每个 “chunk” 接收的记录数。作为第二个参数传递的闭包将为从数据库中检索的每个 chunk 调用。将执行数据库查询以检索传递给 closure 的每个记录块。
如果要根据在迭代结果时也将更新的列筛选 chunk
方法的结果,则应使用 chunkById
方法。在这些情况下使用 chunk
方法可能会导致意外和不一致的结果。在内部,chunkById
方法将始终检索 id
列大于前一个 chunk 中最后一个模型的模型:
php
Flight::where('departed', true)
->chunkById(200, function (Collection $flights) {
$flights->each->update(['departed' => false]);
}, $column = 'id');
使用 Lazy Collections 进行分块
惰性
方法的工作方式类似于 chunk
方法,因为它在幕后以块的形式执行查询。但是,惰性
方法不是按原样将每个块直接传递到回调中,而是返回一个扁平化的 Eloquent 模型的 LazyCollection
,这使您可以将结果作为单个流进行交互:
php
use App\Models\Flight;
foreach (Flight::lazy() as $flight) {
// ...
}
如果要根据在迭代结果时也将更新的列筛选 lazy
方法的结果,则应使用 lazyById
方法。在内部,lazyById
方法将始终检索 id
列大于上一个 chunk 中最后一个模型的模型:
php
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 生成器来实现此功能:
php
use App\Models\Flight;
foreach (Flight::where('destination', 'Zurich')->cursor() as $flight) {
// ...
}
游标
返回一个 Illuminate\Support\LazyCollection
实例。惰性集合允许你使用典型 Laravel 集合上可用的许多集合方法,同时一次只将单个模型加载到内存中:
php
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 还提供高级子查询支持,允许您在单个查询中从相关表中提取信息。例如,假设我们有一个航班目的地
表和一个飞往目的地的航班
表。flights
表包含一个 arrived_at
列,该列指示航班到达目的地的时间。
使用查询生成器的 select
和 addSelect
方法可用的子查询功能,我们可以使用单个查询选择所有目的地
和最近到达该目的地的航班名称:
php
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
函数支持子查询。继续使用我们的航班示例,我们可能会使用此功能根据最后一个航班到达目的地的时间对所有目的地进行排序。同样,这可以在执行单个数据库查询时完成
php
return Destination::orderByDesc(
Flight::select('arrived_at')
->whereColumn('destination_id', 'destinations.id')
->orderByDesc('arrived_at')
->limit(1)
)->get();
检索单个模型/聚合
除了检索与给定查询匹配的所有记录外,您还可以使用 find
、first
或 firstWhere
方法检索单个记录。这些方法不是返回模型集合,而是返回单个模型实例:
php
use App\Models\Flight;
// 按主键检索模型...
$flight = Flight::find(1);
// 检索与查询约束匹配的第一个模型...
$flight = Flight::where('active', 1)->first();
// 检索与查询约束匹配的第一个模型的替代方法...
$flight = Flight::firstWhere('active', 1);
有时,如果未找到结果,您可能希望执行其他操作。findOr
和 firstOr
方法将返回单个模型实例,或者,如果未找到结果,则执行给定的闭包。闭包返回的值将被视为该方法的结果:
php
$flight = Flight::findOr(1, function () {
// ...
});
$flight = Flight::where('legs', '>', 3)->firstOr(function () {
// ...
});
未找到异常
有时,如果未找到模型,您可能希望引发异常。这在 routes 或 controller 中特别有用。findOrFail
和 firstOrFail
方法将检索查询的第一个结果;但是,如果未找到结果,则会抛出 an Illuminate\Database\Eloquent\ModelNotFoundException
:
php
$flight = Flight::findOrFail(1);
$flight = Flight::where('legs', '>', 3)->firstOrFail();
如果未捕获 ModelNotFoundException
,则会自动将 404 HTTP 响应发送回客户端:
php
use App\Models\Flight;
Route::get('/api/flights/{id}', function (string $id) {
return Flight::findOrFail($id);
});
检索或创建模型
firstOrCreate
方法将尝试使用给定的列 / 值对查找数据库记录。如果在数据库中找不到模型,则将插入一条记录,其中包含将第一个数组参数与可选的第二个数组参数合并所产生的属性:
firstOrNew
方法(如 firstOrCreate
)将尝试在数据库中查找与给定属性匹配的记录。但是,如果未找到模型,则将返回新的模型实例。请注意,firstOrNew
返回的模型尚未保存到数据库中。您需要手动调用 save
方法来持久保存它:
php
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 = Flight::firstOrNew([
'name' => 'London to Paris'
]);
// 按 name 检索航班或使用 name、delayed 和 arrival_time 属性实例化...
$flight = Flight::firstOrNew(
['name' => 'Tokyo to Sydney'],
['delayed' => 1, 'arrival_time' => '11:30']
);
检索聚合
在与 Eloquent 模型交互时,您还可以使用 Laravel
查询构建器提供的 count、sum
、max
和其他聚合方法。如您所料,这些方法返回一个标量值,而不是 Eloquent 模型实例:
php
$count = Flight::where('active', 1)->count();
$max = Flight::where('active', 1)->max('price');
插入和更新模型
插入
当然,在使用 Eloquent 时,我们不仅需要从数据库中检索模型。我们还需要插入新记录。值得庆幸的是,Eloquent 让它变得简单。要将新记录插入数据库,您应该实例化一个新的模型实例并在模型上设置属性。然后,在模型实例上调用 save
方法:
php
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Flight;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class FlightController extends Controller
{
/**
* 在数据库中存储新航班
*/
public function store(Request $request): RedirectResponse
{
// 验证请求...
$flight = new Flight;
$flight->name = $request->name;
$flight->save();
return redirect('/flights');
}
}
在此示例中,我们将传入 HTTP 请求的 name
字段分配给 App\Models\Flight
模型实例的 name
属性。当我们调用 save
方法时,一条记录将入到数据库中。当调用 save
方法时,模型的 created_at
和 updated_at
时间戳将自动设置,因此无需手动设置。
或者,你可以使用 create
方法通过单个 PHP 语句 “保存” 新模型。插入的模型实例将通过 create
方法返回给你:
php
use App\Models\Flight;
$flight = Flight::create([
'name' => 'London to Paris',
]);
但是,在使用 create
方法之前,您需要在模型类上指定 fillable
或 guarded
属性。这些属性是必需的,因为默认情况下,所有 Eloquent 模型都受到保护,免受大规模分配漏洞的影响。要了解有关批量分配的更多信息,请查阅批量分配文档。
更新
save
方法还可用于更新数据库中已存在的模型。要更新模型,您应该检索它并设置要更新的任何属性。然后,您应该调用模型的 save
方法。同样,updated_at
时间戳将自动更新,因此无需手动设置其值:
php
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,该航班具有将第一个参数数组与第二个参数数组合并后产生的属性:
php
$flight = Flight::updateOrCreate(
['departure' => 'Oakland', 'destination' => 'San Diego'],
['price' => 99, 'discounted' => 1]
);
批量更新
还可以对与给定查询匹配的模型执行更新。在此示例中,所有在售
且目的地
为圣地亚哥
的航班都将被标记为延误:
php
Flight::where('active', 1)
->where('destination', 'San Diego')
->update(['delayed' => 1]);
update
方法需要一个列对和值对数组,这些对表示应更新的列。update
方法返回受影响的行数。
WARNING
通过 Eloquent 发布批量更新时,不会为更新的模型触发保存
、保存
、更新
和更新的
模型事件。这是因为在发出批量更新时,实际上从未检索过这些模型。
检查属性更改
Eloquent 提供了 isDirty
、isClean
和 wasChanged
方法来检查模型的内部状态,并确定其属性与最初检索模型时相比是如何变化的。
isDirty
方法确定在检索模型后是否更改了模型的任何属性。您可以将特定属性名称或属性数组传递给 isDirty
方法,以确定是否有任何属性是“脏的”。isClean
方法将确定自检索模型以来属性是否保持不变。此方法还接受一个可选的 attribute 参数:
php
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
方法确定在当前请求周期内上次保存模型时是否更改了任何属性。如果需要,您可以传递属性名称以查看特定属性是否已更改:
php
$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
方法返回一个包含模型原始属性的数组,而不管自检索模型以来模型是否发生任何更改。如果需要,您可以传递特定的 attribute name 来获取特定 attribute 的原始值:
php
$user = User::find(1);
$user->name; // John
$user->email; // john@example.com
$user->name = "Jack";
$user->name; // Jack
$user->getOriginal('name'); // John
$user->getOriginal(); // Array of original attributes...
批量分配
您可以使用 create
方法使用单个 PHP 语句 “保存” 新模型。插入的模型实例将通过以下方法返回给您:
php
use App\Models\Flight;
$flight = Flight::create([
'name' => 'London to Paris',
]);
但是,在使用 create
方法之前,您需要在模型类上指定 fillable
或 guarded
属性。这些属性是必需的,因为默认情况下,所有 Eloquent 模型都受到保护,免受大规模分配漏洞的影响。
当用户传递意外的 HTTP 请求字段,并且该字段更改了数据库中您意想不到的列时,就会发生批量分配漏洞。例如,恶意用户可能会通过 HTTP 请求发送 is_admin
参数,然后将其传递给模型的 create
方法,从而允许用户将自己上报给管理员。
因此,首先,您应该定义要使质量可分配的模型属性。您可以使用模型上的 $fillable
属性来执行此操作。例如,让我们将 Flight
模型 mass 的 name
属性设为可分配:
php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 可批量分配的属性
*
* @var array
*/
protected $fillable = ['name'];
}
指定哪些属性可批量分配后,可以使用 create
方法在数据库中插入新记录。create
方法返回新创建的模型实例:
php
$flight = Flight::create(['name' => 'London to Paris']);
如果你已经有一个模型实例,你可以使用 fill
方法用一个属性数组填充它:
php
$flight->fill(['name' => 'Amsterdam to Frankfurt']);
批量分配和 JSON 列
分配 JSON 列时,必须在模型的 $fillable
数组中指定每列的 mass 可分配键。为了安全起见,Laravel 不支持在使用 guarded
属性时更新嵌套的 JSON 属性:
php
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'options->enabled',
];
允许批量分配
如果你想让你的所有属性都可以批量分配,你可以将模型的 $guarded
属性定义为一个空数组。如果你选择取消对模型的约束,你应该特别小心,始终手动创建传递给 Eloquent 的 fill
、create
和 update
方法的数组:
php
/**
* 不可批量分配的属性
*
* @var array
*/
protected $guarded = [];
批量分配异常
默认情况下,在执行 mass-assignment
操作时,将以静默方式丢弃 $fillable 数组中未包含的属性。在生产环境中,这是预期行为;但是,在本地开发过程中,可能会导致对模型更改未生效的原因产生混淆。
如果您愿意,您可以指示 Laravel 在尝试通过调用 preventSilentlyDiscardingAttributes
该方法填充 unfillable 属性时抛出异常。通常,应在应用程序的 AppServiceProvider
类的 boot
方法中调用此方法:
php
use Illuminate\Database\Eloquent\Model;
/**
* 引导任何应用程序服务
*/
public function boot(): void
{
Model::preventSilentlyDiscardingAttributes($this->app->isLocal());
}
更新插入
Eloquent 的 upsert
方法可用于在单个原子操作中更新或创建记录。该方法的第一个参数由要插入或更新的值组成,而第二个参数列出了唯一标识关联表中记录的列。该方法的第三个也是最后一个参数是一个列数组,如果数据库中已存在匹配的记录,则应更新该列。如果在模型上启用了时间戳,则 upsert
方法将自动设置 created_at
和 updated_at
时间戳:
php
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
方法的第二个参数中的列具有“primary”或“unique”索引。此外,MariaDB 和 MySQL 数据库驱动程序忽略 upsert
方法的第二个参数,并始终使用表的 “primary” 和 “unique” 索引来检测现有记录。
删除模型
要删除模型,你可以在模型实例上调用 delete
方法:
php
use App\Models\Flight;
$flight = Flight::find(1);
$flight->delete();
您可以调用 truncate
方法来删除模型的所有关联数据库记录。truncate
操作还将重置模型关联表上的任何自动递增 ID:
php
Flight::truncate();
按主键删除现有模型
在上面的示例中,我们在调用 delete
方法之前从数据库中检索模型。但是,如果您知道模型的主键,则可以删除模型,而无需通过调用 destroy
方法显式检索它。除了接受单个主键之外,destroy
方法还将接受多个主键、主键数组或主键集合:
php
Flight::destroy(1);
Flight::destroy(1, 2, 3);
Flight::destroy([1, 2, 3]);
Flight::destroy(collect([1, 2, 3]));
如果您正在使用软删除模型,您可以通过 forceDestroy
方法永久删除模型:
php
Flight::forceDestroy(1);
WARNING
destroy
方法单独加载每个模型并调用 delete
方法,以便为每个模型正确调度 deleting
和 deleted
事件。
使用查询删除模型
当然,您可以构建一个 Eloquent 查询来删除所有符合查询条件的模型。在此示例中,我们将删除所有标记为非活动的航班。与批量更新一样,批量删除不会为已删除的模型分派模型事件:
php
$deleted = Flight::where('active', 0)->delete();
WARNING
通过 Eloquent 执行批量删除语句时,不会为已删除的模型调度删除
和已删除
的模型事件。这是因为在执行 delete 语句时,实际上从未检索过模型。
软删除
除了实际从数据库中删除记录外,Eloquent 还可以“软删除”模型。软删除模型时,它们实际上不会从数据库中删除。相反,在模型上设置了一个 deleted_at
属性,指示模型被“删除”的日期和时间。要为模型启用软删除,请将 Illuminate\Database\Eloquent\SoftDeletes
特征添加到模型:
php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Flight extends Model
{
use SoftDeletes;
}
NOTE
SoftDeletes
特征会自动将 deleted_at
属性强制转换为 DateTime
/ Carbon
实例。
您还应该将 deleted_at
列添加到数据库表中。Laravel 数据迁移包含用于创建此列的帮助程序方法:
php
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
方法:
php
if ($flight->trashed()) {
// ...
}
恢复软删除的模型
有时,您可能希望“取消删除”软删除的模型。要恢复软删除的模型,您可以在模型实例上调用 restore
方法。restore
方法会将模型的 deleted_at
列设置为 null
:
php
$flight->restore();
您还可以在查询中使用 restore
方法还原多个模型。同样,与其他 “mass” 操作一样,这不会为恢复的模型分派任何模型事件:
php
Flight::withTrashed()
->where('airline_id', 1)
->restore();
在构建关系查询时,也可以使用 restore
方法:
php
$flight->history()->restore();
永久删除模型
有时,您可能需要真正从数据库中删除模型。您可以使用 forceDelete
方法从数据库表中永久删除软删除的模型:
php
$flight->forceDelete();
在构建 Eloquent 关系查询时,您还可以使用 forceDelete
方法:
php
$flight->history()->forceDelete();
查询软删除模型
包括软删除模型
如上所述,软删除的模型将自动从查询结果中排除。但是,您可以通过对查询调用 withTrashed
方法来强制将软删除的模型包含在查询的结果中:
php
use App\Models\Flight;
$flights = Flight::withTrashed()
->where('account_id', 1)
->get();
在构建关系查询时,也可以调用 withTrashed
方法:
php
$flight->history()->withTrashed()->get();
仅检索软删除的模型
onlyTrashed
方法将仅检索软删除的模型:
php
$flights = Flight::onlyTrashed()
->where('airline_id', 1)
->get();
修剪模型
有时,您可能希望定期删除不再需要的模型。为此,您可以将 Illuminate\Database\Eloquent\Prunable
or Illuminate\Database\Eloquent\MassPrunable
特征添加到要定期修剪的模型中。将其中一个特征添加到模型后,实现一个 prunable
方法,该方法返回一个 Eloquent 查询生成器,用于解析不再需要的模型:
php
<?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;
/**
* 获取 prunable 模型查询
*/
public function prunable(): Builder
{
return static::where('created_at', '<=', now()->subMonth());
}
}
将模型标记为 Prunable
时,您还可以在模型上定义修剪
方法。该方法会在删除模型之前调用。在从数据库中永久删除模型之前,此方法可用于删除与模型关联的任何其他资源,例如存储的文件:
php
/**
* 准备模型以进行修剪
*/
protected function pruning(): void
{
// ...
}
配置 prunable 模型后,您应该在应用程序的 routes/console.php
文件中安排 model:prune
Artisan 命令。您可以自由选择运行此命令的适当间隔:
php
use Illuminate\Support\Facades\Schedule;
Schedule::command('model:prune')->daily();
在后台,model:prune
命令将自动检测应用程序的 app/Models
目录中的 “Prunable” 模型。如果你的模型位于不同的位置,你可以使用 --model
选项来指定模型类名称:
php
Schedule::command('model:prune', [
'--model' => [Address::class, Flight::class],
])->daily();
如果你希望在修剪所有其他检测到的模型时排除某些模型,你可以使用 --except
选项:
php
Schedule::command('model:prune', [
'--except' => [Address::class, Flight::class],
])->daily();
您可以通过执行带有 --pretend
选项的 model:prune
命令来测试您的 可优化
查询。当假装时,model:prune
命令将简单地报告如果命令实际运行,将修剪多少条记录:
shell
php artisan model:prune --pretend
WARNING
如果软删除模型与 prunable 查询匹配,则将被永久删除 (forceDelete
)。
批量修剪
当模型标记有特征 Illuminate\Database\Eloquent\MassPrunable
时,将使用批量删除查询从数据库中删除模型。因此,不会调用 pruning
方法,也不会调度 deleting
和 deleted
模型事件。这是因为在删除之前从未实际检索过模型,因此使修剪过程更加高效:
php
<?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;
/**
* 获取 prunable 模型查询
*/
public function prunable(): Builder
{
return static::where('created_at', '<=', now()->subMonth());
}
}
复制模型
您可以使用 replicate
方法创建现有模型实例的未保存副本。当您的模型实例共享许多相同属性时,此方法特别有用:
php
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
方法:
php
$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
目录中:
shell
php artisan make:scope AncientScope
编写全局范围
编写全局范围很简单。首先,使用 make:scope
命令生成实现该接口的 Illuminate\Database\Eloquent\Scope
类。Scope
接口要求您实现一种方法:apply
。apply
方法可以根据需要向查询中添加 where
constraints 或其他类型的子句:
php
<?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));
}
}
NOTE
如果全局范围正在向查询的 select 子句添加列,则应使用 addSelect
方法而不是 select
。这将防止无意中替换查询的现有 select 子句。
应用全局范围
要为模型分配全局范围,您只需将 ScopedBy
属性放在模型上:
php
<?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
方法接受 scope 的实例作为其唯一参数:
php
<?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 查询:
sql
select * from `users` where `created_at` < 0021-02-18 00:00:00
匿名全局范围
Eloquent 还允许你使用闭包定义全局作用域,这对于不需要自己的单独类的简单作用域特别有用。当使用闭包定义全局范围时,你应该提供你自己选择的范围名称作为 addGlobalScope
方法的第一个参数:
php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 模型的 “booted” 方法
*/
protected static function booted(): void
{
static::addGlobalScope('ancient', function (Builder $builder) {
$builder->where('created_at', '<', now()->subYears(2000));
});
}
}
删除全局范围
如果要删除给定查询的全局范围,可以使用 withoutGlobalScope
方法。此方法接受全局范围的类名作为其唯一参数:
php
User::withoutGlobalScope(AncientScope::class)->get();
或者,如果你使用闭包定义了全局范围,你应该传递你分配给全局范围的字符串名称:
php
User::withoutGlobalScope('ancient')->get();
如果要删除查询的多个甚至全部全局范围,可以使用 withoutGlobalScopes
方法:
php
// 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
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 将查询的范围限定为仅包含热门用户
*/
public function scopePopular(Builder $query): void
{
$query->where('votes', '>', 100);
}
/**
* Scope a query to only include active users
*/
public function scopeActive(Builder $query): void
{
$query->where('active', 1);
}
}
利用本地范围
定义 scope 后,您可以在查询模型时调用 scope 方法。但是,在调用该方法时,不应包含 scope
前缀。您甚至可以将调用链接到各种范围:
php
use App\Models\User;
$users = User::popular()->active()->orderBy('created_at')->get();
通过 or
查询运算符组合多个 Eloquent 模型范围可能需要使用闭包来实现正确的逻辑分组:
php
$users = User::popular()->orWhere(function (Builder $query) {
$query->active();
})->get();
但是,由于这可能很麻烦,Laravel 提供了一个 “更高阶” 或 Where
方法,允许您流畅地将范围链接在一起,而无需使用闭包:
php
$users = User::popular()->orWhere->active()->get();
动态范围
有时您可能希望定义一个接受参数的 scope。要开始使用,只需将其他参数添加到 scope 方法的签名中即可。范围参数应在 $query
参数之后定义:
php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 将查询的范围限定为仅包含给定类型的用户
*/
public function scopeOfType(Builder $query, string $type): void
{
$query->where('type', $type);
}
}
将预期的参数添加到 scope 方法的签名后,您可以在调用 scope 时传递参数:
php
$users = User::ofType('admin')->get();
比较模型
有时,您可能需要确定两个模型是否“相同”。is
和 isNot
方法可用于快速验证两个模型是否具有相同的主键、表和数据库连接:
php
if ($post->is($anotherPost)) {
// ...
}
if ($post->isNot($anotherPost)) {
// ...
}
is
和 isNot
方法在使用 belongsTo
、hasOne
、morphTo
和 morphOne
关系时也可用。当您想比较相关模型而不发出查询来检索该模型时,此方法特别有用:
php
if ($post->author()->is($user)) {
// ...
}
事件
NOTE
想要将 Eloquent 事件直接广播到客户端应用程序?查看 Laravel 的模型活动广播。
Eloquent 模型会调度多个事件,允许您挂接到模型生命周期中的以下时刻: retrieved
, creating
, created
, updating
, updated
, saving
, saved
, deleting
, deleted
, trashed
, forceDeleting
, forceDeleted
, restoring
, restored
, and replicating
.
当从数据库中检索到现有模型时,将调度 retrieved
的事件。首次保存新模型时,将调度 creating
和 created
事件。当修改现有模型并调用 save
方法时,将调度 updating
/ updated
事件。保存
/ 保存
事件将在创建或更新模型时调度 - 即使模型的属性尚未更改。以 -ing
结尾的事件名称在持久化对模型的任何更改之前调度,而以 -ed
结尾的事件在持久化对模型的更改之后调度。
要开始监听模型事件,请在 Eloquent 模型上定义一个 $dispatchesEvents
属性。此属性将 Eloquent 模型生命周期的各个点映射到您自己的事件类。每个模型事件类都应该通过其构造函数接收受影响模型的实例:
php
<?php
namespace App\Models;
use App\Events\UserDeleted;
use App\Events\UserSaved;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use Notifiable;
/**
* 模型的事件图
*
* @var array<string, string>
*/
protected $dispatchesEvents = [
'saved' => UserSaved::class,
'deleted' => UserDeleted::class,
];
}
定义和映射 Eloquent 事件后,你可以使用事件侦听器来处理事件。
WARNING
通过 Eloquent 发出批量更新或删除查询时,不会为受影响的模型调度已保存
、更新
、删除
和已删除
的模型事件。这是因为在执行批量更新或删除时,实际上从未检索过模型。
使用闭包
您可以注册在调度各种模型事件时执行的闭包,而不是使用自定义事件类。通常,你应该在模型的 booted
方法中注册这些闭包:
php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 模型的 “booted” 方法
*/
protected static function booted(): void
{
static::created(function (User $user) {
// ...
});
}
}
如果需要,您可以在注册模型事件时使用可排队的匿名事件侦听器。这将指示 Laravel 使用应用程序的队列在后台执行模型事件侦听器:
php
use function Illuminate\Events\queueable;
static::created(queueable(function (User $user) {
// ...
}));
观察员
定义观察者
如果您正在侦听给定模型上的许多事件,则可以使用 observer 将所有侦听器分组到一个类中。Observer 类具有反映您希望监听的 Eloquent 事件的方法名称。这些方法中的每一个都接收受影响的模型作为其唯一参数。make:observer
Artisan 命令是创建新的观察者类的最简单方法:
shell
php artisan make:observer UserObserver --model=User
此命令会将新 observer 放置在 app/Observers
目录中。如果此目录不存在,Artisan 将为您创建该目录。您的新 Observer 将如下所示:
php
<?php
namespace App\Observers;
use App\Models\User;
class UserObserver
{
/**
* 处理 User “created” 事件
*/
public function created(User $user): void
{
// ...
}
/**
* 处理 User “updated” 事件。
*/
public function updated(User $user): void
{
// ...
}
/**
* 处理 User “deleted” 事件
*/
public function deleted(User $user): void
{
// ...
}
/**
* 处理 User “restored” 事件
*/
public function restored(User $user): void
{
// ...
}
/**
* 处理 User “forceDeleted” 事件
*/
public function forceDeleted(User $user): void
{
// ...
}
}
要注册观察者,您可以将 ObservedBy
属性放在相应的模型上:
php
use App\Observers\UserObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
#[ObservedBy([UserObserver::class])]
class User extends Authenticatable
{
//
}
或者,您可以通过在要观察的模型上调用 observe
方法来手动注册观察者。您可以在应用程序的 AppServiceProvider
类的 boot
方法中注册观察者:
php
use App\Models\User;
use App\Observers\UserObserver;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
User::observe(UserObserver::class);
}
NOTE
观察者还可以侦听其他事件,例如 saving
和 retrieved
。事件文档中介绍了这些事件。
观察者和数据库事务
在数据库事务中创建模型时,您可能希望指示观察者仅在提交数据库事务后执行其事件处理程序。您可以通过在观察者上实现 ShouldHandleEventsAfterCommit
接口来实现此目的。如果数据库事务未进行,则事件处理程序将立即执行
php
<?php
namespace App\Observers;
use App\Models\User;
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;
class UserObserver implements ShouldHandleEventsAfterCommit
{
/**
* 处理 User “created” 事件
*/
public function created(User $user): void
{
// ...
}
}
静音事件
您可能偶尔需要暂时 “静音” 模型触发的所有事件。您可以使用 withoutEvents
方法实现此目的。withoutEvents
方法接受闭包作为其唯一参数。在此 closure 中执行的任何代码都不会 dispatch 模型事件,并且 this closure 返回的任何值都将由 withoutEvents
方法返回:
php
use App\Models\User;
$user = User::withoutEvents(function () {
User::findOrFail(1)->delete();
return User::find(2);
});
保存不带事件的单个模型
有时你可能希望 “保存” 给定的模型而不 dispatch 任何事件。您可以使用 saveQuietly
方法完成此操作:
php
$user = User::findOrFail(1);
$user->name = 'Victoria Faith';
$user->saveQuietly();
你也可以 “update”、“delete”、“soft delete”、“restore” 和 “replicate” 给定模型,而不分派任何事件:
php
$user->deleteQuietly();
$user->forceDeleteQuietly();
$user->restoreQuietly();