Eloquent:入门
简介
Laravel 包含 Eloquent,一个对象关系映射器 (ORM),它使与数据库交互变得愉快。使用 Eloquent 时,每个数据库表都有一个相应的“模型”用于与该表交互。除了从数据库表中检索记录外,Eloquent 模型还允许您插入、更新和删除表中的记录。
在开始之前,请确保在应用程序的 config/database.php
配置文件中配置数据库连接。有关配置数据库的更多信息,请查看 数据库配置文档。
Laravel 集训营
如果您是 Laravel 新手,请随时参加 Laravel 集训营。Laravel 集训营将指导您使用 Eloquent 构建第一个 Laravel 应用程序。这是了解 Laravel 和 Eloquent 提供的所有内容的好方法。
生成模型类
首先,让我们创建一个 Eloquent 模型。模型通常位于 app\Models
目录中,并扩展 Illuminate\Database\Eloquent\Model
类。您可以使用 make:model
Artisan 命令 生成一个新的模型
php artisan make:model Flight
如果您想在生成模型时生成 数据库迁移,可以使用 --migration
或 -m
选项
php artisan make:model Flight --migration
在生成模型时,您还可以生成各种其他类型的类,例如工厂、播种器、策略、控制器和表单请求。此外,这些选项可以组合起来一次创建多个类
# Generate a model and a FlightFactory class...php artisan make:model Flight --factoryphp artisan make:model Flight -f # Generate a model and a FlightSeeder class...php artisan make:model Flight --seedphp artisan make:model Flight -s # Generate a model and a FlightController class...php artisan make:model Flight --controllerphp artisan make:model Flight -c # Generate a model, FlightController resource class, and form request classes...php artisan make:model Flight --controller --resource --requestsphp 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 --allphp artisan make:model Flight -a # Generate a pivot model...php artisan make:model Member --pivotphp artisan make:model Member -p
检查模型
有时,仅通过浏览模型的代码可能很难确定所有可用的属性和关系。相反,请尝试使用 model:show
Artisan 命令,它提供模型的所有属性和关系的便捷概述
php artisan model:show Flight
Eloquent 模型约定
make:model
命令生成的模型将放置在 app/Models
目录中。让我们检查一个基本的模型类并讨论一些 Eloquent 的关键约定
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model{ // ...}
表名
浏览上面的示例后,您可能已经注意到我们没有告诉 Eloquent 哪个数据库表对应于我们的 Flight
模型。按照约定,类的“蛇形命名法”复数形式将用作表名,除非显式指定了其他名称。因此,在本例中,Eloquent 将假设 Flight
模型将记录存储在 flights
表中,而 AirTrafficController
模型将记录存储在 air_traffic_controllers
表中。
如果模型对应的数据库表不符合此约定,您可以通过在模型上定义 table
属性来手动指定模型的表名
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model{ /** * The table associated with the model. * * @var string */ protected $table = 'my_flights';}
主键
Eloquent 还假设每个模型对应的数据库表都有一个名为 id
的主键列。如有必要,您可以在模型上定义一个受保护的 $primaryKey
属性以指定用作模型主键的不同列
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model{ /** * The primary key associated with the table. * * @var string */ protected $primaryKey = 'flight_id';}
此外,Eloquent 假设主键是递增的整数值,这意味着 Eloquent 将自动将主键转换为整数。如果您希望使用非递增或非数字主键,则必须在模型上定义一个公共的 $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 primary key 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 对于索引数据库存储更有效,因为它们可以按字典顺序排序。
您可以通过在模型上定义 newUniqueId
方法来覆盖给定模型的 UUID 生成过程。此外,您可以通过在模型上定义 uniqueIds
方法来指定哪些列应接收 UUID
use Ramsey\Uuid\Uuid; /** * Generate a new UUID for the model. */public function newUniqueId(): string{ return (string) Uuid::uuid4();} /** * Get the columns that should receive a unique identifier. * * @return array<int, string> */public function uniqueIds(): array{ return ['id', 'discount_code'];}
如果您愿意,可以选择使用“ULID”而不是 UUID。ULID 类似于 UUID;但是,它们只有 26 个字符长。与有序 UUID 一样,ULID 对于有效的数据库索引是可按字典顺序排序的。要使用 ULID,您应该在模型上使用 Illuminate\Database\Eloquent\Concerns\HasUlids
特性。您还应该确保模型具有 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 自动管理这些列,则应在模型上定义一个值为 false
的 $timestamps
属性
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model{ /** * Indicates if the model should be timestamped. * * @var bool */ public $timestamps = false;}
如果您需要自定义模型时间戳的格式,请设置模型上的 $dateFormat
属性。此属性确定日期属性如何在数据库中存储以及在模型序列化为数组或 JSON 时的格式
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model{ /** * The storage format of the model's date columns. * * @var string */ protected $dateFormat = 'U';}
如果您需要自定义用于存储时间戳的列的名称,可以在模型上定义 CREATED_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 = 'mysql';}
默认属性值
默认情况下,新实例化的模型对象不会包含任何属性值。如果您希望为模型的一些属性定义默认值,可以在模型上定义一个$attributes
属性。放置在$attributes
数组中的属性值应采用原始的、“可存储”的格式,就像它们刚刚从数据库中读取一样。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model{ /** * The model's default values for attributes. * * @var array */ protected $attributes = [ 'options' => '[]', 'delayed' => false, ];}
配置 Eloquent 严格性
Laravel 提供了几种方法,允许您在各种情况下配置 Eloquent 的行为和“严格性”。
首先,preventLazyLoading
方法接受一个可选的布尔参数,指示是否应阻止延迟加载。例如,您可能希望仅在非生产环境中禁用延迟加载,以便即使生产代码中意外存在延迟加载的关系,您的生产环境也能正常运行。通常,此方法应在应用程序的 AppServiceProvider
的 boot
方法中调用。
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 模型视为一个强大的查询构建器,允许您流畅地查询与模型关联的数据库表。模型的 all
方法将检索模型关联的数据库表中的所有记录。
use App\Models\Flight; foreach (Flight::all() as $flight) { echo $flight->name;}
构建查询
Eloquent 的 all
方法将返回模型表中的所有结果。但是,由于每个 Eloquent 模型都充当查询构建器,因此您可以向查询添加其他约束,然后调用 get
方法检索结果。
$flights = Flight::where('active', 1) ->orderBy('name') ->take(10) ->get();
由于 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"
集合
正如我们所见,像 all
和 get
这样的 Eloquent 方法从数据库中检索多条记录。但是,这些方法不会返回普通的 PHP 数组。而是返回 Illuminate\Database\Eloquent\Collection
的一个实例。
Eloquent 的 Collection
类扩展了 Laravel 的基础 Illuminate\Support\Collection
类,该类提供了一系列有用的方法来与数据集合进行交互。例如,reject
方法可用于根据调用的闭包的结果从集合中删除模型。
$flights = Flight::where('destination', 'Paris')->get(); $flights = $flights->reject(function (Flight $flight) { return $flight->cancelled;});
除了 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
方法不会将每个块直接传递给回调,而是返回一个扁平化的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]);
您可以使用 lazyByIdDesc
方法根据 id
的降序过滤结果。
游标
与 lazy
方法类似,cursor
方法可用于在迭代数万条 Eloquent 模型记录时显着降低应用程序的内存消耗。
cursor
方法只会执行一个数据库查询;但是,各个 Eloquent 模型只有在实际迭代时才会被水化。因此,在迭代游标时,任何时候都只保留一个 Eloquent 模型在内存中。
由于 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; // 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');
插入和更新模型
插入
当然,在使用 Eloquent 时,我们不仅需要从数据库中检索模型。我们还需要插入新记录。谢天谢地,Eloquent 使这变得简单。要将新记录插入数据库,应实例化一个新的模型实例并在模型上设置属性。然后,在模型实例上调用 save
方法。
<?php namespace App\Http\Controllers; use App\Http\Controllers\Controller;use App\Models\Flight;use Illuminate\Http\RedirectResponse;use Illuminate\Http\Request; class FlightController extends Controller{ /** * Store a new flight in the database. */ public function store(Request $request): RedirectResponse { // Validate the request... $flight = new Flight; $flight->name = $request->name; $flight->save(); return redirect('/flights'); }}
在此示例中,我们将来自传入 HTTP 请求的 name
字段分配给 App\Models\Flight
模型实例的 name
属性。当我们调用 save
方法时,将向数据库中插入一条记录。模型的 created_at
和 updated_at
时间戳将在调用 save
方法时自动设置,因此无需手动设置它们。
或者,您可以使用 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]);
批量更新
还可以对与给定查询匹配的模型执行更新。在此示例中,所有为active
且destination
为San Diego
的航班都将被标记为延迟。
Flight::where('active', 1) ->where('destination', 'San Diego') ->update(['delayed' => 1]);
update
方法需要一个列和值对数组,表示应该更新的列。update
方法返回受影响的行数。
通过 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->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 请求字段并且该字段更改了您数据库中您未预期的列时。例如,恶意用户可能会通过 HTTP 请求发送is_admin
参数,然后将其传递给模型的create
方法,从而允许用户将自己提升为管理员。
因此,要开始,您应该定义要使哪些模型属性可大规模赋值。您可以使用模型上的$fillable
属性来执行此操作。例如,让我们使Flight
模型的name
属性可大规模赋值。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model{ /** * The attributes that are mass assignable. * * @var array */ protected $fillable = ['name'];}
指定哪些属性可大规模赋值后,您可以使用create
方法在数据库中插入新记录。create
方法返回新创建的模型实例。
$flight = Flight::create(['name' => 'London to Paris']);
如果您已经有模型实例,则可以使用fill
方法用属性数组填充它。
$flight->fill(['name' => 'Amsterdam to Frankfurt']);
大规模赋值和 JSON 列
在赋值 JSON 列时,必须在模型的$fillable
数组中指定每个列的可大规模赋值键。出于安全考虑,Laravel 在使用guarded
属性时不支持更新嵌套的 JSON 属性。
/** * 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
数组中的属性会被静默丢弃。在生产环境中,这是预期的行为;但是,在本地开发过程中,这可能会导致对模型更改为何未生效感到困惑。
如果需要,您可以通过调用preventSilentlyDiscardingAttributes
方法来指示 Laravel 在尝试填充不可填充属性时抛出异常。通常,此方法应在应用程序的AppServiceProvider
类的boot
方法中调用。
use Illuminate\Database\Eloquent\Model; /** * Bootstrap any application services. */public function boot(): void{ Model::preventSilentlyDiscardingAttributes($this->app->isLocal());}
Upserts
Eloquent 的upsert
方法可用于在单个原子操作中更新或创建记录。该方法的第一个参数包含要插入或更新的值,而第二个参数列出唯一标识关联表中记录的列。该方法的第三个也是最后一个参数是一个数组,其中包含如果数据库中已存在匹配记录则应更新的列。如果模型启用了时间戳,则upsert
方法会自动设置created_at
和updated_at
时间戳。
Flight::upsert([ ['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99], ['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150]], uniqueBy: ['departure', 'destination'], update: ['price']);
除了 SQL Server 之外的所有数据库都要求upsert
方法的第二个参数中的列具有“主键”或“唯一”索引。此外,MariaDB 和 MySQL 数据库驱动程序会忽略upsert
方法的第二个参数,并始终使用表的“主键”和“唯一”索引来检测现有记录。
删除模型
要删除模型,您可以在模型实例上调用delete
方法。
use App\Models\Flight; $flight = Flight::find(1); $flight->delete();
您可以调用truncate
方法删除模型关联的所有数据库记录。truncate
操作还将重置模型关联表上的任何自动递增 ID。
Flight::truncate();
根据主键删除现有模型
在上面的示例中,我们在调用delete
方法之前从数据库中检索模型。但是,如果您知道模型的主键,则可以通过调用destroy
方法删除模型,而无需显式检索它。除了接受单个主键外,destroy
方法还将接受多个主键、主键数组或主键集合。
Flight::destroy(1); Flight::destroy(1, 2, 3); Flight::destroy([1, 2, 3]); Flight::destroy(collect([1, 2, 3]));
如果您正在使用软删除模型,则可以通过forceDestroy
方法永久删除模型。
Flight::forceDestroy(1);
destroy
方法会单独加载每个模型并调用delete
方法,以便为每个模型正确分派deleting
和deleted
事件。
使用查询删除模型
当然,您可以构建一个 Eloquent 查询来删除与查询条件匹配的所有模型。在此示例中,我们将删除所有标记为非活动状态的航班。与批量更新一样,批量删除不会为删除的模型分派模型事件。
$deleted = Flight::where('active', 0)->delete();
通过 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;}
SoftDeletes
特性会自动为您将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
特性添加到您希望定期修剪的模型中。在将其中一个特性添加到模型后,实现一个prunable
方法,该方法返回一个 Eloquent 查询构建器,该构建器解析不再需要的模型。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Builder;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Prunable; class Flight extends Model{ use Prunable; /** * Get the prunable model query. */ public function prunable(): Builder { return static::where('created_at', '<=', now()->subMonth()); }}
在将模型标记为Prunable
时,您还可以为模型定义一个pruning
方法。此方法将在删除模型之前被调用。此方法可用于在模型从数据库中永久删除之前删除与模型关联的任何其他资源,例如存储的文件。
/** * Prepare the model for pruning. */protected function pruning(): void{ // ...}
配置可修剪模型后,您应该在应用程序的routes/console.php
文件中安排model:prune
Artisan 命令。您可以自由选择运行此命令的适当间隔。
use Illuminate\Support\Facades\Schedule; Schedule::command('model:prune')->daily();
在幕后,model:prune
命令将自动检测应用程序的app/Models
目录中的“可修剪”模型。如果您的模型位于其他位置,则可以使用--model
选项指定模型类名。
Schedule::command('model:prune', [ '--model' => [Address::class, Flight::class],])->daily();
如果您希望在修剪所有其他检测到的模型时排除某些模型不被修剪,则可以使用--except
选项。
Schedule::command('model:prune', [ '--except' => [Address::class, Flight::class],])->daily();
您可以通过执行带有--pretend
选项的model:prune
命令来测试您的prunable
查询。在模拟模式下,model:prune
命令只会报告如果实际运行该命令将修剪多少条记录。
php artisan model:prune --pretend
如果软删除的模型与可修剪查询匹配,则将被永久删除(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 自己的软删除功能利用全局作用域仅从数据库中检索“未删除”的模型。编写您自己的全局作用域可以提供一种方便、简单的方法来确保针对给定模型的每个查询都接收某些约束。
生成作用域
要生成一个新的全局作用域,您可以调用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{ /** * Apply the scope to a given Eloquent query builder. */ public function apply(Builder $builder, Model $model): void { $builder->where('created_at', '<', now()->subYears(2000)); }}
如果您的全局作用域正在向查询的 select 子句中添加列,则应使用addSelect
方法而不是select
。这将防止意外替换查询现有的 select 子句。
应用全局作用域
要将全局作用域分配给模型,您只需在模型上放置ScopedBy
属性即可。
<?php namespace App\Models; use App\Models\Scopes\AncientScope;use Illuminate\Database\Eloquent\Attributes\ScopedBy; #[ScopedBy([AncientScope::class])]class User extends Model{ //}
或者,您可以通过覆盖模型的booted
方法并调用模型的addGlobalScope
方法来手动注册全局作用域。addGlobalScope
方法仅接受您的作用域实例作为其参数。
<?php namespace App\Models; use App\Models\Scopes\AncientScope;use Illuminate\Database\Eloquent\Model; class User extends Model{ /** * The "booted" method of the model. */ protected static function booted(): void { static::addGlobalScope(new AncientScope); }}
在将上面的示例中的作用域添加到App\Models\User
模型后,对User::all()
方法的调用将执行以下 SQL 查询。
select * from `users` where `created_at` < 0021-02-18 00:00:00
匿名全局作用域
Eloquent 还允许您使用闭包定义全局作用域,这对于不需要单独类的简单作用域特别有用。当使用闭包定义全局作用域时,您应该将您自己选择的作用域名称作为第一个参数传递给addGlobalScope
方法。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Builder;use Illuminate\Database\Eloquent\Model; class User extends Model{ /** * The "booted" method of the model. */ protected static function booted(): void { static::addGlobalScope('ancient', function (Builder $builder) { $builder->where('created_at', '<', now()->subYears(2000)); }); }}
移除全局作用域
如果您想为给定查询移除全局作用域,可以使用withoutGlobalScope
方法。此方法仅接受全局作用域的类名作为其参数。
User::withoutGlobalScope(AncientScope::class)->get();
或者,如果您使用闭包定义了全局作用域,则应传递您分配给全局作用域的字符串名称。
User::withoutGlobalScope('ancient')->get();
如果您想移除多个甚至所有查询的全局作用域,可以使用withoutGlobalScopes
方法。
// Remove all of the global scopes...User::withoutGlobalScopes()->get(); // Remove some of the global scopes...User::withoutGlobalScopes([ FirstScope::class, SecondScope::class])->get();
本地范围
局部作用域允许您定义一组通用的查询约束,您可以在整个应用程序中轻松地重复使用它们。例如,您可能需要经常检索所有被认为是“热门”的用户。要定义一个作用域,请在 Eloquent 模型方法前加上scope
。
作用域应始终返回相同的查询构建器实例或void
。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Builder;use Illuminate\Database\Eloquent\Model; class User extends Model{ /** * Scope a query to only include popular users. */ public function scopePopular(Builder $query): void { $query->where('votes', '>', 100); } /** * Scope a query to only include active users. */ public function scopeActive(Builder $query): void { $query->where('active', 1); }}
利用局部作用域
定义作用域后,您可以在查询模型时调用作用域方法。但是,在调用方法时不应包含scope
前缀。您甚至可以将对各种作用域的调用链接起来。
use App\Models\User; $users = User::popular()->active()->orderBy('created_at')->get();
通过or
查询运算符组合多个 Eloquent 模型作用域可能需要使用闭包才能实现正确的逻辑分组。
$users = User::popular()->orWhere(function (Builder $query) { $query->active();})->get();
但是,由于这可能很麻烦,Laravel 提供了一个“高阶”orWhere
方法,允许您流畅地将作用域链接在一起,而无需使用闭包。
$users = User::popular()->orWhere->active()->get();
动态作用域
有时您可能希望定义一个接受参数的作用域。要开始使用,只需将您额外的参数添加到作用域方法的签名中即可。作用域参数应在$query
参数之后定义。
<?php namespace App\Models; use Illuminate\Database\Eloquent\Builder;use Illuminate\Database\Eloquent\Model; class User extends Model{ /** * Scope a query to only include users of a given type. */ public function scopeOfType(Builder $query, string $type): void { $query->where('type', $type); }}
将预期参数添加到作用域方法的签名后,您可以在调用作用域时传递这些参数。
$users = User::ofType('admin')->get();
比较模型
有时您可能需要确定两个模型是否“相同”。is
和isNot
方法可用于快速验证两个模型是否具有相同的 primary key、表和数据库连接。
if ($post->is($anotherPost)) { // ...} if ($post->isNot($anotherPost)) { // ...}
is
和isNot
方法在使用belongsTo
、hasOne
、morphTo
和morphOne
关系时也可用。当您想比较相关模型而不发出查询来检索该模型时,此方法特别有用。
if ($post->author()->is($user)) { // ...}
事件
想要将您的 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; /** * The event map for the model. * * @var array<string, string> */ protected $dispatchesEvents = [ 'saved' => UserSaved::class, 'deleted' => UserDeleted::class, ];}
定义和映射您的 Eloquent 事件后,您可以使用事件监听器来处理这些事件。
通过 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 使用应用程序的队列在后台执行模型事件监听器。
use function Illuminate\Events\queueable; static::created(queueable(function (User $user) { // ...}));
观察者
定义观察者
如果您正在监听给定模型上的许多事件,则可以使用观察者将所有监听器分组到一个类中。观察者类具有反映您希望监听的 Eloquent 事件的方法名称。这些方法中的每一个都接收受影响的模型作为其唯一参数。make:observer
Artisan 命令是创建新的观察者类的最简单方法。
php artisan make:observer UserObserver --model=User
此命令会将新的观察者放置在您的app/Observers
目录中。如果此目录不存在,Artisan 将为您创建它。您的新观察者将如下所示。
<?php namespace App\Observers; use App\Models\User; class UserObserver{ /** * Handle the User "created" event. */ public function created(User $user): void { // ... } /** * Handle the User "updated" event. */ public function updated(User $user): void { // ... } /** * Handle the User "deleted" event. */ public function deleted(User $user): void { // ... } /** * Handle the User "restored" event. */ public function restored(User $user): void { // ... } /** * Handle the User "forceDeleted" event. */ public function forceDeleted(User $user): void { // ... }}
要注册观察者,您可以在相应的模型上放置ObservedBy
属性。
use App\Observers\UserObserver;use Illuminate\Database\Eloquent\Attributes\ObservedBy; #[ObservedBy([UserObserver::class])]class User extends Authenticatable{ //}
或者,您可以通过在要观察的模型上调用observe
方法来手动注册观察者。您可以在应用程序的AppServiceProvider
类的boot
方法中注册观察者。
use App\Models\User;use App\Observers\UserObserver; /** * Bootstrap any application services. */public function boot(): void{ User::observe(UserObserver::class);}
观察者可以监听其他事件,例如saving
和retrieved
。这些事件在事件文档中进行了描述。
观察者和数据库事务
当模型在数据库事务中创建时,您可能希望指示观察者仅在数据库事务提交后才执行其事件处理程序。您可以通过在观察者上实现ShouldHandleEventsAfterCommit
接口来实现此目的。如果没有数据库事务正在进行,则事件处理程序将立即执行。
<?php namespace App\Observers; use App\Models\User;use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit; class UserObserver implements ShouldHandleEventsAfterCommit{ /** * Handle the User "created" event. */ public function created(User $user): void { // ... }}
静音事件
您可能偶尔需要暂时“静音”模型触发的所有事件。您可以使用withoutEvents
方法实现此目的。withoutEvents
方法仅接受闭包作为其参数。在此闭包中执行的任何代码都不会分派模型事件,并且闭包返回的任何值都将由withoutEvents
方法返回。
use App\Models\User; $user = User::withoutEvents(function () { User::findOrFail(1)->delete(); return User::find(2);});
保存单个模型而不触发事件
有时您可能希望“保存”给定模型而不分派任何事件。您可以使用saveQuietly
方法实现此目的。
$user = User::findOrFail(1); $user->name = 'Victoria Faith'; $user->saveQuietly();
您还可以“更新”、“删除”、“软删除”、“恢复”和“复制”给定模型而不分派任何事件。
$user->deleteQuietly();$user->forceDeleteQuietly();$user->restoreQuietly();