雄辩:入门
Introduction
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
工匠命令 生成新模型:
php artisan make:model Flight
如果你想生成一个数据库迁移 当你生成模型时,你可以使用--migration
或者-m
选项:
php artisan make:model Flight --migration
在生成模型时,您可能会生成各种其他类型的类,例如工厂、播种器、策略、控制器和表单请求。此外,这些选项可以结合起来一次创建多个类:
# Generate a model and a FlightFactory class...
php artisan make:model Flight --factory
php artisan make:model Flight -f
# Generate a model and a FlightSeeder class...
php artisan make:model Flight --seed
php artisan make:model Flight -s
# Generate a model and a FlightController class...
php artisan make:model Flight --controller
php artisan make:model Flight -c
# Generate a model, FlightController resource class, and form request classes...
php artisan make:model Flight --controller --resource --requests
php artisan make:model Flight -crR
# Generate a model and a FlightPolicy class...
php artisan make:model Flight --policy
# Generate a model and a migration, factory, seeder, and controller...
php artisan make:model Flight -mfsc
# Shortcut to generate a model, migration, factory, seeder, policy, controller, and form requests...
php artisan make:model Flight --all
# Generate a pivot model...
php artisan make:model Member --pivot
检查模型
有时,仅通过略读代码很难确定模型的所有可用属性和关系。相反,尝试model:show
Artisan 命令,它提供了所有模型属性和关系的便捷概览:
php artisan model:show Flight
雄辩的模型约定
生成的模型make:model
命令将被放置在app/Models
目录。让我们检查一个基本的模型类并讨论 Eloquent 的一些关键约定:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
// ...
}
表名
看了上面的例子,你可能已经注意到,我们并没有告诉 Eloquent 哪个数据库表对应于我们的Flight
模型。按照惯例,除非明确指定另一个名称,否则“snake case”,类的复数名称将用作表名。所以,在这种情况下,Eloquent 将假设Flight
模型将记录存储在flights
表,而一个AirTrafficController
模型会将记录存储在air_traffic_controllers
桌子。
如果您的模型对应的数据库表不符合此约定,您可以通过定义一个手动指定模型的表名table
模型属性:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'my_flights';
}
主键
Eloquent 还会假设每个模型对应的数据库表都有一个名为id
.如有必要,您可以定义一个受保护的$primaryKey
模型上的属性以指定用作模型主键的不同列:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'flight_id';
}
此外,Eloquent 假定主键是一个递增的整数值,这意味着 Eloquent 会自动将主键转换为整数。如果你想使用一个非递增或非数字的主键,你必须定义一个公共的$incrementing
模型上的属性设置为false
:
<?php
class Flight extends Model
{
/**
* Indicates if the model's ID is auto-incrementing.
*
* @var bool
*/
public $incrementing = false;
}
如果你的模型的主键不是整数,你应该定义一个受保护的$keyType
您模型上的属性。此属性的值应为string
:
<?php
class Flight extends Model
{
/**
* The data type of the auto-incrementing ID.
*
* @var string
*/
protected $keyType = 'string';
}
“复合”主键
Eloquent 要求每个模型至少有一个可以作为其主键的唯一标识“ID”。 Eloquent 模型不支持“复合”主键。但是,除了表的唯一标识主键之外,您还可以自由地向数据库表添加额外的多列唯一索引。
UUID 和 ULID 密钥
您可以选择使用 UUID,而不是使用自动递增的整数作为 Eloquent 模型的主键。 UUID 是普遍唯一的字母数字标识符,长度为 36 个字符。
如果您希望模型使用 UUID 键而不是自动递增的整数键,您可以使用Illuminate\Database\Eloquent\Concerns\HasUuids
模型上的特征。当然,你应该确保模型有一个UUID 等效主键列:
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
use HasUuids;
// ...
}
$article = Article::create(['title' => 'Traveling to Europe']);
$article->id; // "8f8e8478-9035-4d23-b9a7-62f4d2612ce5"
默认情况下,HasUuids
特质会产生“有序”UUID 为您的模型。这些 UUID 对于索引数据库存储更有效,因为它们可以按字典顺序排序。
您可以通过定义一个给定模型来覆盖 UUID 生成过程newUniqueId
模型上的方法。此外,您可以通过定义一个uniqueIds
模型上的方法:
use Ramsey\Uuid\Uuid;
/**
* Generate a new UUID for the model.
*/
public function newUniqueId(): string
{
return (string) Uuid::uuid4();
}
/**
* Get the columns that should receive a unique identifier.
*
* @return array<int, string>
*/
public function uniqueIds(): array
{
return ['id', 'discount_code'];
}
如果您愿意,您可以选择使用“ULID”而不是 UUID。 ULID 类似于 UUID;但是,它们的长度只有 26 个字符。与有序的 UUID 一样,ULID 可以按字典顺序排序以实现高效的数据库索引。要使用 ULID,您应该使用Illuminate\Database\Eloquent\Concerns\HasUlids
模型的特征。您还应该确保模型具有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"
Timestamps
默认情况下,Eloquent 期望created_at
和updated_at
列存在于模型的相应数据库表中。 Eloquent 会在创建或更新模型时自动设置这些列的值。如果你不想让 Eloquent 自动管理这些列,你应该定义一个$timestamps
模型上的属性值为false
:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
}
如果您需要自定义模型时间戳的格式,请设置$dateFormat
您模型上的属性。此属性确定日期属性在数据库中的存储方式以及模型序列化为数组或 JSON 时的格式:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* The storage format of the model's date columns.
*
* @var string
*/
protected $dateFormat = 'U';
}
如果您需要自定义用于存储时间戳的列的名称,您可以定义CREATED_AT
和UPDATED_AT
模型上的常量:
<?php
class Flight extends Model
{
const CREATED_AT = 'creation_date';
const UPDATED_AT = 'updated_date';
}
如果您想在没有模型的情况下执行模型操作updated_at
时间戳已修改,您可以在给定的闭包内对模型进行操作withoutTimestamps
方法:
Model::withoutTimestamps(fn () => $post->increment(['reads']));
数据库连接
默认情况下,所有 Eloquent 模型都将使用为您的应用程序配置的默认数据库连接。如果您想指定与特定模型交互时应使用的不同连接,您应该定义一个$connection
模型属性:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* The database connection that should be used by the model.
*
* @var string
*/
protected $connection = 'sqlite';
}
默认属性值
默认情况下,新实例化的模型实例将不包含任何属性值。如果您想为模型的某些属性定义默认值,您可以定义一个$attributes
您模型上的属性。属性值放在$attributes
数组应该是原始的、“可存储的”格式,就好像它们刚从数据库中读取一样:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* The model's default values for attributes.
*
* @var array
*/
protected $attributes = [
'options' => '[]',
'delayed' => false,
];
}
配置雄辩的严格性
Laravel 提供了几种方法,允许您在各种情况下配置 Eloquent 的行为和“严格性”。
首先,preventLazyLoading
方法接受一个可选的布尔参数,指示是否应阻止延迟加载。例如,您可能希望只在非生产环境中禁用延迟加载,这样即使在生产代码中意外出现延迟加载关系,您的生产环境也能继续正常运行。通常,应在boot
你的应用程序的方法AppServiceProvider
:
use Illuminate\Database\Eloquent\Model;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}
此外,您可以指示 Laravel 在尝试通过调用来填充无法填充的属性时抛出异常preventSilentlyDiscardingAttributes
方法。这有助于防止在本地开发期间尝试设置尚未添加到模型的属性时出现意外错误fillable
大批:
Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());
最后,您可以指示 Eloquent 抛出一个异常,如果您试图访问模型上的一个属性,而该属性实际上并未从数据库中检索到,或者该属性不存在。例如,当您忘记将属性添加到select
Eloquent 查询的子句:
Model::preventAccessingMissingAttributes(! $this->app->isProduction());
启用雄辩的“严格模式”
为了方便起见,您可以通过简单地调用shouldBeStrict
方法:
Model::shouldBeStrict(! $this->app->isProduction());
检索模型
一旦你创建了一个模型并且其关联的数据库表,您已准备好开始从数据库中检索数据。您可以将每个 Eloquent 模型视为一个强大的查询生成器 允许您流畅地查询与模型关联的数据库表。模特的all
方法将从模型的关联数据库表中检索所有记录:
use App\Models\Flight;
foreach (Flight::all() as $flight) {
echo $flight->name;
}
构建查询
雄辩者all
方法将返回模型表中的所有结果。然而,由于每个 Eloquent 模型都作为一个查询生成器,您可以向查询添加额外的约束,然后调用get
检索结果的方法:
$flights = Flight::where('active', 1)
->orderBy('name')
->take(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"
Collections
正如我们所见,Eloquent 方法如all
和get
从数据库中检索多条记录。但是,这些方法不返回普通的 PHP 数组。相反,一个实例Illuminate\Database\Eloquent\Collection
被退回。
雄辩者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;
}
分块结果
如果您尝试通过加载数万条 Eloquent 记录,您的应用程序可能会耗尽内存all
或者get
方法。而不是使用这些方法,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');
使用惰性集合分块
这lazy
方法类似于这chunk
方法 从某种意义上说,它在幕后以块的形式执行查询。但是,不是将每个块直接传递到回调中,而是lazy
方法返回一个展平的LazyCollection
的 Eloquent 模型,它可以让你将结果作为一个单一的流进行交互:
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]);
您可以根据降序过滤结果id
使用lazyByIdDesc
方法。
Cursors
类似于lazy
方法,cursor
方法可用于显着减少应用程序在遍历数万条 Eloquent 模型记录时的内存消耗。
这cursor
方法将只执行一个数据库查询;然而,在实际迭代之前,各个 Eloquent 模型不会被水化。因此,在任何给定时间迭代游标时,只有一个 Eloquent 模型保留在内存中。
Warning
自从cursor
方法一次只能在内存中保存一个 Eloquent 模型,它不能预先加载关系。如果您需要预先加载关系,请考虑使用这lazy
方法 反而。
在内部,cursor
方法使用PHPgenerators 实现此功能:
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;
// Retrieve a model by its primary key...
$flight = Flight::find(1);
// Retrieve the first model matching the query constraints...
$flight = Flight::where('active', 1)->first();
// Alternative to retrieving the first model matching the query constraints...
$flight = Flight::firstWhere('active', 1);
如果没有找到结果,有时您可能希望执行一些其他操作。这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;
// Retrieve flight by name or create it if it doesn't exist...
$flight = Flight::firstOrCreate([
'name' => 'London to Paris'
]);
// Retrieve flight by name or create it with the name, delayed, and arrival_time attributes...
$flight = Flight::firstOrCreate(
['name' => 'London to Paris'],
['delayed' => 1, 'arrival_time' => '11:30']
);
// Retrieve flight by name or instantiate a new Flight instance...
$flight = Flight::firstOrNew([
'name' => 'London to Paris'
]);
// Retrieve flight by name or instantiate with the name, delayed, and arrival_time attributes...
$flight = Flight::firstOrNew(
['name' => 'Tokyo to Sydney'],
['delayed' => 1, 'arrival_time' => '11:30']
);
检索聚合
当与 Eloquent 模型交互时,你也可以使用count
,sum
,max
, 和别的聚合方法 由 Laravel 提供查询生成器.如您所料,这些方法返回标量值而不是 Eloquent 模型实例:
$count = Flight::where('active', 1)->count();
$max = Flight::where('active', 1)->max('price');
插入和更新模型
Inserts
当然,在使用 Eloquent 时,我们不仅仅需要从数据库中检索模型。我们还需要插入新记录。值得庆幸的是,Eloquent 让它变得简单。要将新记录插入数据库,您应该实例化一个新的模型实例并在模型上设置属性。然后,调用save
模型实例上的方法:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Flight;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class FlightController extends Controller
{
/**
* Store a new flight in the database.
*/
public function store(Request $request): RedirectResponse
{
// Validate the request...
$flight = new Flight;
$flight->name = $request->name;
$flight->save();
return redirect('/flights');
}
}
在这个例子中,我们分配name
从传入的 HTTP 请求到name
的属性App\Models\Flight
模型实例。当我们调用save
方法,一条记录将被插入到数据库中。模特的created_at
和updated_at
时间戳将自动设置时save
方法被调用,因此无需手动设置它们。
或者,您可以使用create
使用单个 PHP 语句“保存”新模型的方法。插入的模型实例将由create
方法:
use App\Models\Flight;
$flight = Flight::create([
'name' => 'London to Paris',
]);
然而,在使用之前create
方法,您需要指定一个fillable
或者guarded
模型类的属性。这些属性是必需的,因为默认情况下,所有 Eloquent 模型都受到保护,免受质量分配漏洞的影响。要了解有关批量分配的更多信息,请参阅批量分配文档.
Updates
这save
方法也可用于更新数据库中已存在的模型。要更新模型,您应该检索它并设置您希望更新的任何属性。然后,您应该调用模型的save
方法。再次,updated_at
timestamp 会自动更新,所以不需要手动设置它的值:
use App\Models\Flight;
$flight = Flight::find(1);
$flight->name = 'Paris to London';
$flight->save();
批量更新
也可以针对匹配给定查询的模型执行更新。在此示例中,所有航班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(); // 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(); // Array of original attributes...
批量分配
您可以使用create
使用单个 PHP 语句“保存”新模型的方法。插入的模型实例将通过以下方法返回给您:
use App\Models\Flight;
$flight = Flight::create([
'name' => 'London to Paris',
]);
然而,在使用之前create
方法,您需要指定一个fillable
或者guarded
模型类的属性。这些属性是必需的,因为默认情况下,所有 Eloquent 模型都受到保护,免受质量分配漏洞的影响。
当用户传递一个意外的 HTTP 请求字段并且该字段更改了您未预料到的数据库中的列时,就会出现批量分配漏洞。例如,恶意用户可能会发送is_admin
通过 HTTP 请求传递参数,然后将其传递给模型的create
方法,允许用户将自己升级为管理员。
因此,首先,您应该定义要使哪些模型属性可批量分配。您可以使用$fillable
模型上的属性。例如,让我们制作name
我们的属性Flight
模型质量分配:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['name'];
}
一旦指定了哪些属性是可批量分配的,就可以使用create
方法在数据库中插入一条新记录。这create
方法返回新创建的模型实例:
$flight = Flight::create(['name' => 'London to Paris']);
如果您已经有一个模型实例,您可以使用fill
用属性数组填充它的方法:
$flight->fill(['name' => 'Amsterdam to Frankfurt']);
批量分配和 JSON 列
分配 JSON 列时,必须在模型的$fillable
大批。为了安全起见,Laravel 不支持在使用时更新嵌套的 JSON 属性guarded
财产:
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'options->enabled',
];
允许批量分配
如果你想让你的所有属性都可以分配,你可以定义你的模型$guarded
属性为空数组。如果你选择取消保护你的模型,你应该特别注意始终手工制作传递给 Eloquent 的数组fill
,create
, 和update
方法:
/**
* The attributes that aren't mass assignable.
*
* @var array
*/
protected $guarded = [];
批量分配异常
默认情况下,不包含在$fillable
执行批量分配操作时,数组会被静默丢弃。在生产中,这是预期的行为;然而,在本地开发过程中,它可能会导致混淆,为什么模型更改没有生效。
如果你愿意,你可以指示 Laravel 在尝试通过调用preventSilentlyDiscardingAttributes
方法。通常,应在boot
您的应用程序服务提供商之一的方法:
use Illuminate\Database\Eloquent\Model;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Model::preventSilentlyDiscardingAttributes($this->app->isLocal());
}
Upserts
有时,如果不存在匹配模型,您可能需要更新现有模型或创建新模型。像firstOrCreate
方法,updateOrCreate
方法保留模型,因此无需手动调用save
方法。
在下面的示例中,如果航班存在departure
的位置Oakland
和一个destination
的位置San Diego
, 它是price
和discounted
列将被更新。如果不存在这样的航班,将创建一个新航班,该航班具有将第一个参数数组与第二个参数数组合并后产生的属性:
$flight = Flight::updateOrCreate(
['departure' => 'Oakland', 'destination' => 'San Diego'],
['price' => 99, 'discounted' => 1]
);
如果您想在单个查询中执行多个“更新插入”,那么您应该使用upsert
方法代替。该方法的第一个参数包含要插入或更新的值,而第二个参数列出了在关联表中唯一标识记录的列。该方法的第三个也是最后一个参数是一个列数组,如果匹配记录已存在于数据库中,则应更新这些列。这upsert
方法将自动设置created_at
和updated_at
如果在模型上启用了时间戳,则为时间戳:
Flight::upsert([
['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99],
['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150]
], ['departure', 'destination'], ['price']);
Warning
除 SQL Server 外的所有数据库都需要第二个参数中的列upsert
具有“主要”或“唯一”索引的方法。此外,MySQL 数据库驱动程序忽略了upsert
方法并始终使用表的“主要”和“唯一”索引来检测现有记录。
删除模型
要删除模型,您可以调用delete
模型实例上的方法:
use App\Models\Flight;
$flight = Flight::find(1);
$flight->delete();
你可以打电话给truncate
方法删除所有模型的关联数据库记录。这truncate
操作还将重置模型关联表上的任何自动递增 ID:
Flight::truncate();
通过主键删除现有模型
在上面的示例中,我们在调用之前从数据库中检索模型delete
方法。但是,如果您知道模型的主键,则可以删除模型而无需通过调用destroy
方法。除了接受单个主键外,destroy
方法将接受多个主键、一个主键数组或一个collection 主键:
Flight::destroy(1);
Flight::destroy(1, 2, 3);
Flight::destroy([1, 2, 3]);
Flight::destroy(collect([1, 2, 3]));
Warning
这destroy
方法单独加载每个模型并调用delete
方法使deleting
和deleted
为每个模型正确分派事件。
使用查询删除模型
当然,您可以构建一个 Eloquent 查询来删除所有符合您的查询条件的模型。在此示例中,我们将删除所有标记为无效的航班。与批量更新一样,批量删除不会为已删除的模型分派模型事件:
$deleted = Flight::where('active', 0)->delete();
Warning
通过 Eloquent 执行批量删除语句时,deleting
和deleted
将不会为已删除的模型分派模型事件。这是因为在执行删除语句时从未真正检索到模型。
软删除
除了实际从数据库中删除记录外,Eloquent 还可以“软删除”模型。当模型被软删除时,它们实际上并没有从您的数据库中删除。相反,一个deleted_at
属性在模型上设置,指示模型被“删除”的日期和时间。要为模型启用软删除,请添加Illuminate\Database\Eloquent\SoftDeletes
模型的特征:
<?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
构建时也可以使用方法relationship 查询:
$flight->history()->restore();
永久删除模型
有时您可能需要真正从数据库中删除模型。您可以使用forceDelete
从数据库表中永久删除软删除模型的方法:
$flight->forceDelete();
您也可以使用forceDelete
构建 Eloquent 关系查询时的方法:
$flight->history()->forceDelete();
查询软删除模型
包括软删除模型
如上所述,软删除模型将自动从查询结果中排除。但是,您可以通过调用withTrashed
查询方法:
use App\Models\Flight;
$flights = Flight::withTrashed()
->where('account_id', 1)
->get();
这withTrashed
方法也可以在构建时调用relationship 询问:
$flight->history()->withTrashed()->get();
仅检索软删除模型
这onlyTrashed
方法将检索only 软删除模型:
$flights = Flight::onlyTrashed()
->where('airline_id', 1)
->get();
修剪模型
有时您可能希望定期删除不再需要的模型。为此,您可以添加Illuminate\Database\Eloquent\Prunable
或者Illuminate\Database\Eloquent\MassPrunable
您希望定期修剪的模型的特征。将其中一个特征添加到模型后,实现一个prunable
返回一个 Eloquent 查询构建器的方法,该构建器解析不再需要的模型:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;
class Flight extends Model
{
use Prunable;
/**
* Get the prunable model query.
*/
public function prunable(): Builder
{
return static::where('created_at', '<=', now()->subMonth());
}
}
将模型标记为Prunable
, 你也可以定义一个pruning
模型上的方法。在删除模型之前将调用此方法。在从数据库中永久删除模型之前,此方法可用于删除与模型关联的任何其他资源,例如存储的文件:
/**
* Prepare the model for pruning.
*/
protected function pruning(): void
{
// ...
}
配置可修剪模型后,您应该安排model:prune
应用程序中的 Artisan 命令App\Console\Kernel
班级。您可以自由选择运行此命令的适当时间间隔:
/**
* Define the application's command schedule.
*/
protected function schedule(Schedule $schedule): void
{
$schedule->command('model:prune')->daily();
}
在幕后,model:prune
命令将自动检测应用程序中的“Prunable”模型app/Models
目录。如果您的模型位于不同的位置,您可以使用--model
指定模型类名称的选项:
$schedule->command('model:prune', [
'--model' => [Address::class, Flight::class],
])->daily();
如果您希望在修剪所有其他检测到的模型时排除某些模型被修剪,您可以使用--except
选项:
$schedule->command('model:prune', [
'--except' => [Address::class, Flight::class],
])->daily();
你可以测试你的prunable
通过执行查询model:prune
命令与--pretend
选项。假装的时候,model:prune
如果命令实际运行,命令将简单地报告将删除多少记录:
php artisan model:prune --pretend
Warning
软删除模型将被永久删除(forceDelete
) 如果它们匹配可修剪查询。
大规模修剪
当型号标有Illuminate\Database\Eloquent\MassPrunable
特征,模型使用批量删除查询从数据库中删除。因此,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;
/**
* Get the prunable model query.
*/
public function prunable(): Builder
{
return static::where('created_at', '<=', now()->subMonth());
}
}
复制模型
您可以使用创建现有模型实例的未保存副本replicate
方法。当您拥有共享许多相同属性的模型实例时,此方法特别有用:
use App\Models\Address;
$shipping = Address::create([
'type' => 'shipping',
'line_1' => '123 Example Street',
'city' => 'Victorville',
'state' => 'CA',
'postcode' => '90001',
]);
$billing = $shipping->replicate()->fill([
'type' => 'billing'
]);
$billing->save();
要排除一个或多个属性被复制到新模型,您可以将一个数组传递给replicate
方法:
$flight = Flight::create([
'destination' => 'LAX',
'origin' => 'LHR',
'last_flown' => '2020-03-04 11:00:00',
'last_pilot_id' => 747,
]);
$flight = $flight->replicate([
'last_flown',
'last_pilot_id'
]);
查询范围
全球范围
全局范围允许您为给定模型的所有查询添加约束。 Laravel 自带的软删除 功能利用全局范围仅从数据库中检索“未删除”模型。编写您自己的全局范围可以提供一种方便、简单的方法来确保给定模型的每个查询都收到特定的约束。
编写全局范围
编写全局范围很简单。首先,定义一个类来实现Illuminate\Database\Eloquent\Scope
界面。 Laravel 没有一个你应该放置范围类的常规位置,所以你可以自由地将这个类放在你想要的任何目录中。
这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
{
/**
* 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 子句。
应用全局范围
要为模型分配全局范围,您应该覆盖模型的booted
方法并调用模型的addGlobalScope
方法。这addGlobalScope
方法接受范围的实例作为其唯一参数:
<?php
namespace App\Models;
use App\Models\Scopes\AncientScope;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* The "booted" method of the model.
*/
protected static function booted(): void
{
static::addGlobalScope(new AncientScope);
}
}
将上面示例中的范围添加到App\Models\User
模型,调用User::all()
方法将执行以下 SQL 查询:
select * from `users` where `created_at` < 0021-02-18 00:00:00
匿名全局范围
Eloquent 还允许您使用闭包定义全局作用域,这对于不需要单独的类的简单作用域特别有用。使用闭包定义全局范围时,您应该提供您自己选择的范围名称作为addGlobalScope
方法:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* The "booted" method of the model.
*/
protected static function booted(): void
{
static::addGlobalScope('ancient', function (Builder $builder) {
$builder->where('created_at', '<', now()->subYears(2000));
});
}
}
删除全局范围
如果您想删除给定查询的全局范围,您可以使用withoutGlobalScope
方法。此方法接受全局范围的类名作为其唯一参数:
User::withoutGlobalScope(AncientScope::class)->get();
或者,如果您使用闭包定义了全局作用域,则应该传递分配给全局作用域的字符串名称:
User::withoutGlobalScope('ancient')->get();
如果你想删除几个甚至所有查询的全局范围,你可以使用withoutGlobalScopes
方法:
// Remove all of the global scopes...
User::withoutGlobalScopes()->get();
// Remove some of the global scopes...
User::withoutGlobalScopes([
FirstScope::class, SecondScope::class
])->get();
本地范围
本地范围允许您定义通用的查询约束集,您可以在整个应用程序中轻松地重复使用这些约束。例如,您可能需要经常检索所有被认为“受欢迎”的用户。要定义范围,请在 Eloquent 模型方法前面加上scope
.
范围应始终返回相同的查询构建器实例或void
:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Scope a query to only include popular users.
*/
public function scopePopular(Builder $query): void
{
$query->where('votes', '>', 100);
}
/**
* Scope a query to only include active users.
*/
public function scopeActive(Builder $query): void
{
$query->where('active', 1);
}
}
使用本地范围
定义范围后,您可以在查询模型时调用范围方法。但是,您不应包括scope
调用方法时的前缀。您甚至可以将调用链接到各种范围:
use App\Models\User;
$users = User::popular()->active()->orderBy('created_at')->get();
通过一个组合多个 Eloquent 模型作用域or
查询运算符可能需要使用闭包来实现正确的逻辑分组:
$users = User::popular()->orWhere(function (Builder $query) {
$query->active();
})->get();
然而,由于这可能很麻烦,Laravel 提供了一个“高阶”orWhere
允许您在不使用闭包的情况下流畅地将作用域链接在一起的方法:
$users = App\Models\User::popular()->orWhere->active()->get();
动态作用域
有时您可能希望定义一个接受参数的范围。要开始,只需将您的附加参数添加到范围方法的签名中。范围参数应在之后定义$query
范围:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Scope a query to only include users of a given type.
*/
public function scopeOfType(Builder $query, string $type): void
{
$query->where('type', $type);
}
}
将预期参数添加到范围方法的签名后,您可以在调用范围时传递参数:
$users = User::ofType('admin')->get();
比较模型
有时您可能需要确定两个模型是否“相同”。这is
和isNot
方法可用于快速验证两个模型是否具有相同的主键、表和数据库连接:
if ($post->is($anotherPost)) {
// ...
}
if ($post->isNot($anotherPost)) {
// ...
}
这is
和isNot
使用时也可以使用方法belongsTo
,hasOne
,morphTo
, 和morphOne
relationships.当您想比较相关模型而不发出查询来检索该模型时,此方法特别有用:
if ($post->author()->is($user)) {
// ...
}
Events
Note
想要将 Eloquent 事件直接广播到客户端应用程序吗?查看 Laravel 的模型事件广播.
Eloquent 模型会分派多个事件,让您可以挂钩模型生命周期中的以下时刻:retrieved
,creating
,created
,updating
,updated
,saving
,saved
,deleting
,deleted
,trashed
,forceDeleting
,forceDeleted
,restoring
,restored
, 和replicating
.
这retrieved
当从数据库中检索到现有模型时,将调度事件。第一次保存新模型时,creating
和created
事件将派发。这updating
/updated
当修改现有模型并且save
方法被调用。这saving
/saved
事件将在创建或更新模型时分派 - 即使模型的属性未更改。事件名称以-ing
在模型的任何更改被持久化之前被分派,而事件以-ed
在持久化对模型的更改后分派。
要开始监听模型事件,定义一个$dispatchesEvents
Eloquent 模型上的属性。此属性将 Eloquent 模型生命周期的各个点映射到您自己的事件类.每个模型事件类都应该期望通过其构造函数接收受影响模型的实例:
<?php
namespace App\Models;
use App\Events\UserDeleted;
use App\Events\UserSaved;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use Notifiable;
/**
* The event map for the model.
*
* @var array
*/
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
{
/**
* The "booted" method of the model.
*/
protected static function booted(): void
{
static::created(function (User $user) {
// ...
});
}
}
如果需要,您可以使用可排队的匿名事件监听器 注册模型事件时。这将指示 Laravel 使用您的应用程序在后台执行模型事件侦听器queue:
use function Illuminate\Events\queueable;
static::created(queueable(function (User $user) {
// ...
}));
Observers
定义观察者
如果您在给定模型上监听许多事件,您可以使用观察者将所有监听器分组到一个类中。观察者类具有反映您希望监听的 Eloquent 事件的方法名称。这些方法中的每一个都接收受影响的模型作为它们唯一的参数。这make:observer
Artisan 命令是创建新观察者类的最简单方法:
php artisan make:observer UserObserver --model=User
这个命令会将新的观察者放在你的app/Observers
目录。如果该目录不存在,Artisan 将为您创建。您的新观察者将如下所示:
<?php
namespace App\Observers;
use App\Models\User;
class UserObserver
{
/**
* Handle the User "created" event.
*/
public function created(User $user): void
{
// ...
}
/**
* Handle the User "updated" event.
*/
public function updated(User $user): void
{
// ...
}
/**
* Handle the User "deleted" event.
*/
public function deleted(User $user): void
{
// ...
}
/**
* Handle the User "restored" event.
*/
public function restored(User $user): void
{
// ...
}
/**
* Handle the User "forceDeleted" event.
*/
public function forceDeleted(User $user): void
{
// ...
}
}
要注册观察者,您需要调用observe
您希望观察的模型上的方法。您可以在boot
你的应用程序的方法App\Providers\EventServiceProvider
服务提供者:
use App\Models\User;
use App\Observers\UserObserver;
/**
* Register any events for your application.
*/
public function boot(): void
{
User::observe(UserObserver::class);
}
或者,您可以在一个列表中列出您的观察员$observers
您的应用程序的属性App\Providers\EventServiceProvider
班级:
use App\Models\User;
use App\Observers\UserObserver;
/**
* The model observers for your application.
*
* @var array
*/
protected $observers = [
User::class => [UserObserver::class],
];
Note
观察者可以收听其他事件,例如saving
和retrieved
.这些事件在events 文档。
观察者和数据库事务
在数据库事务中创建模型时,您可能希望指示观察者仅在提交数据库事务后才执行其事件处理程序。您可以通过定义一个$afterCommit
观察者的财产。如果数据库事务未在进行中,事件处理程序将立即执行:
<?php
namespace App\Observers;
use App\Models\User;
class UserObserver
{
/**
* Handle events after all transactions are committed.
*
* @var bool
*/
public $afterCommit = true;
/**
* Handle the User "created" event.
*/
public function created(User $user): void
{
// ...
}
}
静音事件
您可能偶尔需要暂时“静音”模型触发的所有事件。您可以使用withoutEvents
方法。这withoutEvents
方法接受一个闭包作为它唯一的参数。在此闭包中执行的任何代码都不会分派模型事件,闭包返回的任何值都将由withoutEvents
方法:
use App\Models\User;
$user = User::withoutEvents(function () {
User::findOrFail(1)->delete();
return User::find(2);
});
保存没有事件的单个模型
有时您可能希望在不发送任何事件的情况下“保存”给定模型。您可以使用saveQuietly
方法:
$user = User::findOrFail(1);
$user->name = 'Victoria Faith';
$user->saveQuietly();
您还可以在不发送任何事件的情况下“更新”、“删除”、“软删除”、“恢复”和“复制”给定模型:
$user->deleteQuietly();
$user->forceDeleteQuietly();
$user->restoreQuietly();