Eloquent: 入门
介绍
Laravel 包含 Eloquent,一个对象关系映射器(ORM),使与数据库的交互变得愉快。使用 Eloquent 时,每个数据库表都有一个对应的“模型”用于与该表交互。除了从数据库表中检索记录外,Eloquent 模型还允许您插入、更新和删除表中的记录。
在开始之前,请确保在应用程序的 config/database.php
配置文件中配置数据库连接。有关配置数据库的更多信息,请查看数据库配置文档。
生成模型类
要开始使用,让我们创建一个 Eloquent 模型。模型通常位于 app\Models
目录中,并扩展 Illuminate\Database\Eloquent\Model
类。您可以使用 make:model
Artisan 命令生成一个新模型:
php artisan make:model Flight
如果您希望在生成模型时生成数据库迁移,可以使用 --migration
或 -m
选项:
php artisan make:model Flight --migration
您可以在生成模型时生成各种其他类型的类,例如工厂、填充器、策略、控制器和表单请求。此外,这些选项可以组合在一起以一次创建多个类:
# 生成一个模型和一个 FlightFactory 类...
php artisan make:model Flight --factory
php artisan make:model Flight -f
# 生成一个模型和一个 FlightSeeder 类...
php artisan make:model Flight --seed
php artisan make:model Flight -s
# 生成一个模型和一个 FlightController 类...
php artisan make:model Flight --controller
php artisan make:model Flight -c
# 生成一个模型、FlightController 资源类和表单请求类...
php artisan make:model Flight --controller --resource --requests
php artisan make:model Flight -crR
# 生成一个模型和一个 FlightPolicy 类...
php artisan make:model Flight --policy
# 生成一个模型和迁移、工厂、填充器和控制器...
php artisan make:model Flight -mfsc
# 快捷方式生成一个模型、迁移、工厂、填充器、策略、控制器和表单请求...
php artisan make:model Flight --all
# 生成一个枢纽模型...
php artisan make:model Member --pivot
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
{
/**
* 与模型关联的表。
*
* @var string
*/
protected $table = 'my_flights';
}
主键
Eloquent 还将假定每个模型对应的数据库表有一个名为 id
的主键列。如果需要,您可以在模型上定义一个受保护的 $primaryKey
属性,以指定一个不同的列作为模型的主键:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 与表关联的主键。
*
* @var string
*/
protected $primaryKey = 'flight_id';
}
此外,Eloquent 假定主键是一个递增的整数值,这意味着 Eloquent 将自动将主键转换为整数。如果您希望使用非递增或非数字的主键,您必须在模型上定义一个公共的 $incrementing
属性,并将其设置为 false
:
<?php
class Flight extends Model
{
/**
* 指示模型的 ID 是否是自动递增的。
*
* @var bool
*/
public $incrementing = false;
}
如果您的模型主键不是整数,您应该在模型上定义一个受保护的 $keyType
属性。此属性应具有 string
的值:
<?php
class Flight extends Model
{
/**
* 自动递增 ID 的数据类型。
*
* @var string
*/
protected $keyType = 'string';
}
“复合”主键
Eloquent 要求每个模型至少有一个唯一标识的“ID”可以作为其主键。Eloquent 模型不支持“复合”主键。但是,您可以自由地在数据库表中添加额外的多列唯一索引,除了表的唯一标识主键之外。
时间戳
默认情况下,Eloquent 期望模型对应的数据库表中存在 created_at
和 updated_at
列。Eloquent 将在创建或更新模型时自动设置这些列的值。如果您不希望这些列由 Eloquent 自动管理,您应该在模型上定义一个 $timestamps
属性,并将其值设置为 false
:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 指示模型是否应被时间戳。
*
* @var bool
*/
public $timestamps = false;
}
如果您需要自定义模型时间戳的格式,请在模型上设置 $dateFormat
属性。此属性确定日期属性在数据库中的存储方式以及模型序列化为数组或 JSON 时的格式:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 模型日期列的存储格式。
*
* @var string
*/
protected $dateFormat = 'U';
}
如果您需要自定义用于存储时间戳的列的名称,可以在模型上定义 CREATED_AT
和 UPDATED_AT
常量:
<?php
class Flight extends Model
{
const CREATED_AT = 'creation_date';
const UPDATED_AT = 'updated_date';
}
数据库连接
默认情况下,所有 Eloquent 模型将使用为应用程序配置的默认数据库连接。如果您希望指定与特定模型交互时应使用的不同连接,您应该在模型上定义一个 $connection
属性:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 模型应使用的数据库连接。
*
* @var string
*/
protected $connection = 'sqlite';
}
默认属性值
默认情况下,新实例化的模型实例将不包含任何属性值。如果您希望为模型的某些属性定义默认值,可以在模型上定义一个 $attributes
属性:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 模型的默认属性值。
*
* @var array
*/
protected $attributes = [
'delayed' => false,
];
}
检索模型
一旦您创建了模型和其关联的数据库表,您就可以开始从数据库中检索数据。您可以将每个 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"
集合
正如我们所见,Eloquent 方法如 all
和 get
从数据库中检索多个记录。然而,这些方法不会返回一个普通的 PHP 数组。相反,返回的是 Illuminate\Database\Eloquent\Collection
的一个实例。
Eloquent 的 Collection
类扩展了 Laravel 的基础 Illuminate\Support\Collection
类,该类提供了一系列有用的方法用于与数据集合交互。例如,reject
方法可用于根据调用的闭包结果从集合中移除模型:
$flights = Flight::where('destination', 'Paris')->get();
$flights = $flights->reject(function ($flight) {
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;
Flight::chunk(200, function ($flights) {
foreach ($flights as $flight) {
//
}
});
传递给 chunk
方法的第一个参数是您希望每个“块”接收的记录数。作为第二个参数传递的闭包将在从数据库中检索到的每个块时被调用。将执行一个数据库查询以检索传递给闭包的每个块的记录。
如果您根据一个您将在迭代结果时更新的列过滤 chunk
方法的结果,您应该使用 chunkById
方法。在这些场景中使用 chunk
方法可能会导致意外和不一致的结果。在内部,chunkById
方法将始终检索 id
列大于上一个块中最后一个模型的模型:
Flight::where('departed', true)
->chunkById(200, function ($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]);
您可以使用 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) {
return $user->id > 500;
});
foreach ($users as $user) {
echo $user->id;
}
尽管 cursor
方法使用的内存比常规查询少得多(因为在任何时候只在内存中保留一个 Eloquent 模型),但它最终仍会耗尽内存。这是因为PHP 的 PDO 驱动程序在其缓冲区中内部缓存所有原始查询结果。如果您正在处理大量 Eloquent 记录,请考虑使用lazy
方法。
高级子查询
子查询选择
Eloquent 还提供高级子查询支持,允许您在单个查询中从相关表中提取信息。例如,假设我们有一个航班 destinations
表和一个 flights
表到目的地。flights
表包含一个 arrived_at
列,指示航班到达目的地的时间。
使用查询构建器的 select
和 addSelect
方法提供的子查询功能,我们可以使用单个查询选择所有 destinations
和最近到达该目的地的航班的名称:
use App\Models\Destination;
use App\Models\Flight;
return Destination::addSelect(['last_flight' => Flight::select('name')
->whereColumn('destination_id', 'destinations.id')
->orderByDesc('arrived_at')
->limit(1)
])->get();
子查询排序
此外,查询构建器的 orderBy
函数支持子查询。继续使用我们的航班示例,我们可以使用此功能根据最后一次航班到达目的地的时间对所有目的地进行排序。同样,这可以在执行单个数据库查询时完成:
return Destination::orderByDesc(
Flight::select('arrived_at')
->whereColumn('destination_id', 'destinations.id')
->orderByDesc('arrived_at')
->limit(1)
)->get();
检索单个模型/聚合
除了检索与给定查询匹配的所有记录外,您还可以使用 find
、first
或 firstWhere
方法检索单个记录。这些方法返回一个单一的模型实例,而不是返回一个模型集合:
use App\Models\Flight;
// 根据主键检索模型...
$flight = Flight::find(1);
// 检索与查询约束匹配的第一个模型...
$flight = Flight::where('active', 1)->first();
// 检索与查询约束匹配的第一个模型的替代方法...
$flight = Flight::firstWhere('active', 1);
有时您可能希望检索查询的第一个结果或在未找到结果时执行其他操作。firstOr
方法将返回与查询匹配的第一个结果,或者如果未找到结果,则执行给定的闭包。闭包返回的值将被视为 firstOr
方法的结果:
$model = 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 ($id) {
return Flight::findOrFail($id);
});
检索或创建模型
firstOrCreate
方法将尝试使用给定的列/值对定位数据库记录。如果在数据库中找不到模型,将插入一个与第一个数组参数和可选的第二个数组参数合并的属性的记录:
firstOrNew
方法类似于 firstOrCreate
,将尝试在数据库中定位与给定属性匹配的记录。然而,如果未找到模型,将返回一个新的模型实例。请注意,firstOrNew
返回的模型尚未持久化到数据库。您需要手动调用 save
方法来持久化它:
use App\Models\Flight;
// 根据名称检索航班或在不存在时创建它...
$flight = Flight::firstOrCreate([
'name' => 'London to Paris'
]);
// 根据名称检索航班或使用名称、延迟和到达时间属性创建它...
$flight = Flight::firstOrCreate(
['name' => 'London to Paris'],
['delayed' => 1, 'arrival_time' => '11:30']
);
// 根据名称检索航班或实例化一个新的 Flight 实例...
$flight = Flight::firstOrNew([
'name' => 'London to Paris'
]);
// 根据名称检索航班或实例化具有名称、延迟和到达时间属性的航班...
$flight = Flight::firstOrNew(
['name' => 'Tokyo to Sydney'],
['delayed' => 1, 'arrival_time' => '11:30']
);
检索聚合
与 Eloquent 模型交互时,您还可以使用 Laravel 查询构建器提供的 count
、sum
、max
和其他聚合方法。正如您所期望的,这些方法返回一个标量值,而不是一个 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\Request;
class FlightController extends Controller
{
/**
* 在数据库中存储一个新航班。
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
// 验证请求...
$flight = new Flight;
$flight->name = $request->name;
$flight->save();
}
}
在此示例中,我们将传入的 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();
批量更新
更新也可以针对与给定查询匹配的模型进行。在此示例中,所有 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->isClean(); // false
$user->isClean('title'); // false
$user->isClean('first_name'); // true
$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('first_name'); // false
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(); // 原始属性的数组...
批量赋值
您可以使用 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
{
/**
* 可批量赋值的属性。
*
* @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 属性:
/**
* 可批量赋值的属性。
*
* @var array
*/
protected $fillable = [
'options->enabled',
];
允许批量赋值
如果您希望使所有属性都可批量赋值,您可以将模型的 $guarded
属性定义为空数组。如果您选择取消保护模型,您应该特别注意始终手工制作传递给 Eloquent 的 fill
、create
和 update
方法的数组:
/**
* 不可批量赋值的属性。
*
* @var array
*/
protected $guarded = [];
更新或插入
有时,您可能需要更新现有模型或在不存在匹配模型时创建新模型。与 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']);
删除模型
要删除模型,您可以在模型实例上调用 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]));
destroy
方法会单独加载每个模型并调用 delete
方法,以便为每个模型正确分派 deleting
和 deleted
事件。
使用查询删除模型
当然,您可以构建一个 Eloquent 查询以删除与查询条件匹配的所有模型。在此示例中,我们将删除所有标记为非活动的航班。与批量更新一样,批量删除不会为被删除的模型分派模型事件:
$deleted = Flight::where('active', 0)->delete();
在通过 Eloquent 执行批量删除语句时,不会为被删除的模型分派 deleting
和 deleted
模型事件。这是因为在执行删除语句时,模型实际上从未被检索。
软删除
除了实际从数据库中删除记录外,Eloquent 还可以“软删除”模型。当模型被软删除时,它们实际上并没有从数据库中删除。相反,模型上会设置一个 deleted_at
属性,指示模型被“删除”的日期和时间。要为模型启用软删除,请将 Illuminate\Database\Eloquent\SoftDeletes
trait 添加到模型中:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Flight extends Model
{
use SoftDeletes;
}
SoftDeletes
trait 将自动为您将 deleted_at
属性转换为 DateTime
/ Carbon
实例。
您还应该将 deleted_at
列添加到数据库表中。Laravel schema builder 包含一个帮助方法来创建此列:
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
Schema::table('flights', function (Blueprint $table) {
$table->softDeletes();
});
Schema::table('flights', function (Blueprint $table) {
$table->dropSoftDeletes();
});
现在,当您在模型上调用 delete
方法时,deleted_at
列将被设置为当前日期和时间。然而,模型的数据库记录将保留在表中。当查询使用软删除的模型时,软删除的模型将自动从所有查询结果中排除。
要确定给定的模型实例是否已被软删除,您可以使用 trashed
方法:
if ($flight->trashed()) {
//
}
恢复软删除的模型
有时您可能希望“取消删除”软删除的模型。要恢复软删除的模型,您可以在模型实例上调用 restore
方法。restore
方法将模型的 deleted_at
列设置为 null
:
$flight->restore();
您还可以在查询中使用 restore
方法来恢复多个模型。同样,与其他“批量”操作一样,这不会为恢复的模型分派任何模型事件:
Flight::withTrashed()
->where('airline_id', 1)
->restore();
restore
方法也可以在构建关系查询时使用:
$flight->history()->restore();
永久删除模型
有时您可能需要真正从数据库中删除模型。您可以使用 forceDelete
方法从数据库表中永久删除软删除的模型:
$flight->forceDelete();
您还可以在构建 Eloquent 关系查询时使用 forceDelete
方法:
$flight->history()->forceDelete();
查询软删除模型
包含软删除模型
如上所述,软删除的模型将自动从查询结果中排除。然而,您可以通过在查询上调用 withTrashed
方法强制将软删除的模型包含在查询结果中:
use App\Models\Flight;
$flights = Flight::withTrashed()
->where('account_id', 1)
->get();
withTrashed
方法也可以在构建关系查询时调用:
$flight->history()->withTrashed()->get();
仅检索软删除模型
onlyTrashed
方法将仅检索软删除的模型:
$flights = Flight::onlyTrashed()
->where('airline_id', 1)
->get();
修剪模型
有时您可能希望定期删除不再需要的模型。为此,您可以将 Illuminate\Database\Eloquent\Prunable
或 Illuminate\Database\Eloquent\MassPrunable
trait 添加到您希望定期修剪的模型中。在将其中一个 trait 添加到模型后,实现一个 prunable
方法,该方法返回一个 Eloquent 查询构建器,用于解析不再需要的模型:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;
class Flight extends Model
{
use Prunable;
/**
* 获取可修剪的模型查询。
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function prunable()
{
return static::where('created_at', '<=', now()->subMonth());
}
}
在将模型标记为 Prunable
时,您还可以在模型上定义一个 pruning
方法。此方法将在模型被删除之前调用。此方法可用于在模型从数据库中永久删除之前删除与模型关联的任何其他资源,例如存储的文件:
/**
* 准备模型进行修剪。
*
* @return void
*/
protected function pruning()
{
//
}
在配置好可修剪的模型后,您应该在应用程序的 App\Console\Kernel
类中安排 model:prune
Artisan 命令。您可以自由选择此命令应运行的适当间隔:
/**
* 定义应用程序的命令计划。
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $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
trait 时,模型将使用批量删除查询从数据库中删除。因此,不会调用 pruning
方法,也不会分派 deleting
和 deleted
模型事件。这是因为在删除之前模型实际上从未被检索,从而使修剪过程更加高效:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\MassPrunable;
class Flight extends Model
{
use MassPrunable;
/**
* 获取可修剪的模型查询。
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function prunable()
{
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\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class AncientScope implements Scope
{
/**
* 将作用域应用于给定的 Eloquent 查询构建器。
*
* @param \Illuminate\Database\Eloquent\Builder $builder
* @param \Illuminate\Database\Eloquent\Model $model
* @return void
*/
public function apply(Builder $builder, Model $model)
{
$builder->where('created_at', '<', now()->subYears(2000));
}
}
如果您的全局作用域正在向查询的 select 子句添加列,您应该使用 addSelect
方法而不是 select
。这将防止无意中替换查询的现有 select 子句。
应用全局作用域
要将全局作用域分配给模型,您应该重写模型的 booted
方法并调用模型的 addGlobalScope
方法。addGlobalScope
方法接受您的作用域实例作为其唯一参数:
<?php
namespace App\Models;
use App\Scopes\AncientScope;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 模型的“booted”方法。
*
* @return void
*/
protected static function booted()
{
static::addGlobalScope(new AncientScope);
}
}
在将上面示例中的作用域添加到 App\Models\User
模型后,调用 User::all()
方法将执行以下 SQL 查询:
select * from `users` where `created_at` < 0021-02-18 00:00:00
匿名全局作用域
Eloquent 还允许您使用闭包定义全局作用域,这对于不值得单独类的简单作用域特别有用。在使用闭包定义全局作用域时,您应该提供一个您自己选择的作用域名称作为 addGlobalScope
方法的第一个参数:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 模型的“booted”方法。
*
* @return void
*/
protected static function booted()
{
static::addGlobalScope('ancient', function (Builder $builder) {
$builder->where('created_at', '<', now()->subYears(2000));
});
}
}
移除全局作用域
如果您希望为给定查询移除全局作用域,可以使用 withoutGlobalScope
方法。此方法接受全局作用域的类名作为其唯一参数:
User::withoutGlobalScope(AncientScope::class)->get();
或者,如果您使用闭包定义了全局作用域,您应该传递您分配给全局作用域的字符串名称:
User::withoutGlobalScope('ancient')->get();
如果您希望移除多个甚至所有查询的全局作用域,可以使用 withoutGlobalScopes
方法:
// 移除所有全局作用域...
User::withoutGlobalScopes()->get();
// 移除一些全局作用域...
User::withoutGlobalScopes([
FirstScope::class, SecondScope::class
])->get();
本地作用域
本地作用域允许您定义常见的查询约束集,您可以在应用程序中轻松重用。例如,您可能需要频繁检索所有被认为是“受欢迎”的用户。要定义作用域,请在 Eloquent 模型方法前加上 scope
。
作用域应始终返回相同的查询构建器实例或 void
:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 将查询作用域限制为仅包含受欢迎的用户。
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopePopular($query)
{
return $query->where('votes', '>', 100);
}
/**
* 将查询作用域限制为仅包含活跃的用户。
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return void
*/
public function scopeActive($query)
{
$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 = App\Models\User::popular()->orWhere->active()->get();
动态作用域
有时您可能希望定义一个接受参数的作用域。要开始,只需将您的额外参数添加到作用域方法的签名中。作用域参数应在 $query
参数之后定义:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 将查询作用域限制为仅包含给定类型的用户。
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param mixed $type
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeOfType($query, $type)
{
return $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
关系时也可用。当您希望在不发出查询以检索该模型的情况下比较相关模型时,此方法特别有用:
if ($post->author()->is($user)) {
//
}
事件
想要将您的 Eloquent 事件直接广播到客户端应用程序?查看 Laravel 的模型事件广播。
Eloquent 模型会分派多个事件,允许您在模型生命周期的以下时刻挂钩:retrieved
、creating
、created
、updating
、updated
、saving
、saved
、deleting
、deleted
、restoring
、restored
和 replicating
。
retrieved
事件将在从数据库中检索现有模型时分派。当首次保存新模型时,将分派 creating
和 created
事件。updating
/ updated
事件将在修改现有模型并调用 save
方法时分派。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;
class User extends Authenticatable
{
use Notifiable;
/**
* 模型的事件映射。
*
* @var array
*/
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
{
/**
* 模型的“booted”方法。
*
* @return void
*/
protected static function booted()
{
static::created(function ($user) {
//
});
}
}
如果需要,您可以在注册模型事件时利用可排队的匿名事件监听器。这将指示 Laravel 使用应用程序的队列在后台执行模型事件监听器:
use function Illuminate\Events\queueable;
static::created(queueable(function ($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
{
/**
* 处理用户“created”事件。
*
* @param \App\Models\User $user
* @return void
*/
public function created(User $user)
{
//
}
/**
* 处理用户“updated”事件。
*
* @param \App\Models\User $user
* @return void
*/
public function updated(User $user)
{
//
}
/**
* 处理用户“deleted”事件。
*
* @param \App\Models\User $user
* @return void
*/
public function deleted(User $user)
{
//
}
/**
* 处理用户“forceDeleted”事件。
*
* @param \App\Models\User $user
* @return void
*/
public function forceDeleted(User $user)
{
//
}
}
要注册观察者,您需要在您希望观察的模型上调用 observe
方法。您可以在应用程序的 App\Providers\EventServiceProvider
服务提供者的 boot
方法中注册观察者:
use App\Models\User;
use App\Observers\UserObserver;
/**
* 为应用程序注册任何事件。
*
* @return void
*/
public function boot()
{
User::observe(UserObserver::class);
}
观察者可以监听其他事件,例如 saving
和 retrieved
。这些事件在事件文档中进行了描述。
观察者和数据库事务
当模型在数据库事务中创建时,您可能希望指示观察者仅在数据库事务提交后执行其事件处理程序。您可以通过在观察者上定义一个 $afterCommit
属性来实现此目的。如果数据库事务未进行,事件处理程序将立即执行:
<?php
namespace App\Observers;
use App\Models\User;
class UserObserver
{
/**
* 在所有事务提交后处理事件。
*
* @var bool
*/
public $afterCommit = true;
/**
* 处理用户“created”事件。
*
* @param \App\Models\User $user
* @return void
*/
public function created(User $user)
{
//
}
}
静音事件
您可能偶尔需要暂时“静音”模型触发的所有事件。您可以使用 withoutEvents
方法实现此目的。withoutEvents
方法接受一个闭包作为其唯一参数。在此闭包中执行的任何代码都不会分派模型事件,并且闭包返回的任何值都将由 withoutEvents
方法返回:
use App\Models\User;
$user = User::withoutEvents(function () use () {
User::findOrFail(1)->delete();
return User::find(2);
});
保存单个模型而不触发事件
有时您可能希望“保存”给定的模型而不分派任何事件。您可以使用 saveQuietly
方法实现此目的:
$user = User::findOrFail(1);
$user->name = 'Victoria Faith';
$user->saveQuietly();