快速入门
简介
Laravel 内置了 Eloquent,这是一个对象关系映射(ORM)工具,让你与数据库的交互变得更加轻松愉快。在使用 Eloquent 时,每个数据库表都有一个对应的 “模型(Model)”,用于操作该表。除了从数据库表中检索记录之外,Eloquent 模型还允许你向表中插入、更新和删除记录。
config/database.php 配置文件中配置好数据库连接。有关数据库配置的更多信息,请参阅数据库配置文档。生成模型类
要开始使用 Eloquent,我们首先创建一个模型。模型通常位于 app\Models 目录下,并继承自 Illuminate\Database\Eloquent\Model 类。你可以使用 Artisan 命令 make:model 来生成一个新的模型:
php artisan make:model Flight
如果希望在生成模型的同时创建数据库迁移文件,可以使用 --migration 或简写 -m 选项:
php artisan make:model Flight --migration
在生成模型时,你还可以创建其他类型的类,例如工厂(Factory)、数据库填充器(Seeder)、策略(Policy)、控制器(Controller)和表单请求(Form Request)。这些选项也可以组合使用,一次生成多个类:
# 生成模型和 FlightFactory 类
php artisan make:model Flight --factory
php artisan make:model Flight -f
# 生成模型和 FlightSeeder 类
php artisan make:model Flight --seed
php artisan make:model Flight -s
# 生成模型和 FlightController 类
php artisan make:model Flight --controller
php artisan make:model Flight -c
# 生成模型、FlightController 资源类和表单请求类
php artisan make:model Flight --controller --resource --requests
php artisan make:model Flight -crR
# 生成模型和 FlightPolicy 类
php artisan make:model Flight --policy
# 生成模型、迁移、工厂、填充器和控制器
php artisan make:model Flight -mfsc
# 快捷生成模型、迁移、工厂、填充器、策略、控制器和表单请求
php artisan make:model Flight --all
php artisan make:model Flight -a
# 生成一个中间表模型(Pivot Model)
php artisan make:model Member --pivot
php 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 会使用类名的「蛇形命名法(snake_case)」复数形式作为表名。因此,在本例中,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
{
/**
* 指示模型的主键是否自增。
*
* @var bool
*/
public $incrementing = false;
}
如果模型的主键不是整数,还应在模型中定义一个受保护的 $keyType 属性,并将其值设置为 string:
<?php
class Flight extends Model
{
/**
* 主键 ID 的数据类型。
*
* @var string
*/
protected $keyType = 'string';
}
“复合”主键
Eloquent 要求每个模型至少有一个唯一标识的 “ID” 作为主键。Eloquent 模型不支持 “复合” 主键(即由多列组成的主键)。不过,你可以在数据库表中为其他多列组合创建唯一索引,同时保留表的唯一主键。
UUID 与 ULID 主键
除了使用自增整数作为 Eloquent 模型的主键外,你还可以选择使用 UUID(通用唯一标识符)。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;
/**
* 为模型生成新的 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 特性,并确保模型有对应的 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 自动管理这些时间戳列,可以在模型中定义 $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';
}
如果希望在执行模型操作时不修改模型的 updated_at 时间戳,可以使用 withoutTimestamps 方法,将操作放在闭包中执行:
Model::withoutTimestamps(fn () => $post->increment('reads'));
数据库连接
默认情况下,所有 Eloquent 模型都会使用应用程序配置的默认数据库连接。如果你希望在操作特定模型时使用不同的数据库连接,可以在模型中定义 $connection 属性:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 模型应使用的数据库连接。
*
* @var string
*/
protected $connection = 'mysql';
}
默认属性值
默认情况下,新创建的模型实例不会包含任何属性值。如果你希望为模型的某些属性定义默认值,可以在模型中定义 $attributes 属性。放在 $attributes 数组中的值应为原始的“可存储”格式,就像它们刚从数据库中读取一样:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 模型属性的默认值。
*
* @var array
*/
protected $attributes = [
'options' => '[]',
'delayed' => false,
];
}
配置 Eloquent 严格模式
Laravel 提供了多种方法来配置 Eloquent 的行为和“严格性”,以应对不同的使用场景。
首先,preventLazyLoading 方法接受一个可选的布尔参数,用于指示是否应禁止懒加载。例如,你可能希望仅在非生产环境中禁用懒加载,这样即使在生产代码中意外出现懒加载关系,也不会影响生产环境的正常运行。通常,这个方法应在应用的 AppServiceProvider 的 boot 方法中调用:
use Illuminate\Database\Eloquent\Model;
/**
* 启动应用服务。
*/
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}
此外,你还可以通过调用 preventSilentlyDiscardingAttributes 方法,指示 Laravel 在尝试填充不可填充属性时抛出异常。这有助于在本地开发时防止意外错误,例如尝试设置尚未加入模型 $fillable 数组的属性:
Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());
检索模型
一旦你创建了模型及其对应的数据库表,就可以开始从数据库中检索数据。你可以将每个 Eloquent 模型视为一个功能强大的查询构建器,用于流畅地查询与该模型关联的数据库表。模型的 all 方法会检索模型关联表中的所有记录:
use App\Models\Flight;
foreach (Flight::all() as $flight) {
echo $flight->name;
}
构建查询
Eloquent 的 all 方法会返回模型表中的所有结果。不过,由于每个 Eloquent 模型本身就是一个查询构建器,你可以在查询中添加额外条件,然后调用 get 方法来获取结果:
$flights = Flight::where('active', 1)
->orderBy('name')
->limit(10)
->get();
由于 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 集合类继承自 Laravel 的基础集合类 Illuminate\Support\Collection,该基础类提供了丰富的方法来操作数据集合。例如,reject 方法可用于根据闭包返回的结果从集合中移除模型:
$flights = Flight::where('destination', 'Paris')->get();
$flights = $flights->reject(function (Flight $flight) {
return $flight->cancelled;
});
除了基础集合类提供的方法之外,Eloquent 集合类还提供了一些专门用于处理 Eloquent 模型集合的额外方法。
由于 Laravel 的所有集合都实现了 PHP 的可迭代接口,你可以像遍历数组一样遍历集合:
foreach ($flights as $flight) {
echo $flight->name;
}
分块处理结果
如果你尝试通过 all 或 get 方法加载成千上万条 Eloquent 记录,应用可能会耗尽内存。与其使用这些方法,不如使用 chunk 方法更高效地处理大量模型。
chunk 方法会按子集检索 Eloquent 模型,并将其传递给闭包进行处理。由于每次只检索当前子集的模型,因此在处理大量模型时,chunk 方法可以显著降低内存使用:
use App\Models\Flight;
use Illuminate\Database\Eloquent\Collection;
Flight::chunk(200, function (Collection $flights) {
foreach ($flights as $flight) {
// ...
}
});
传递给 chunk 方法的第一个参数是每个“块”要接收的记录数。作为第二个参数的闭包会在检索到每个块时被调用。每个块的记录都会通过单独的数据库查询检索并传递给闭包。
如果你在遍历结果时还会更新某列,并且基于该列筛选 chunk 方法的结果,应该使用 chunkById 方法。在这种场景下使用 chunk 方法可能导致结果不一致。chunkById 方法内部会始终检索 id 列大于前一块中最后一个模型的记录:
Flight::where('departed', true)
->chunkById(200, function (Collection $flights) {
$flights->each->update(['departed' => false]);
}, column: 'id');
由于 chunkById 和 lazyById 方法会在查询中添加自己的 where 条件,通常你应当将自己的条件逻辑封装在闭包中:
Flight::where(function ($query) {
$query->where('delayed', true)->orWhere('cancelled', true);
})->chunkById(200, function (Collection $flights) {
$flights->each->update([
'departed' => false,
'cancelled' => true
]);
}, column: 'id');
使用懒集合分块
lazy 方法的工作原理与 chunk 方法类似:它在后台按块执行查询。但不同的是,lazy 方法不会将每个块直接传入回调,而是返回一个扁平化的 Eloquent 模型 LazyCollection,让你可以像操作单个流一样处理结果:
use App\Models\Flight;
foreach (Flight::lazy() as $flight) {
// ...
}
如果你在遍历结果时还会更新某列,并且基于该列筛选 lazy 方法的结果,应该使用 lazyById 方法。lazyById 方法内部会始终检索 id 列大于前一块中最后一个模型的记录:
Flight::where('departed', true)
->lazyById(200, column: 'id')
->each->update(['departed' => false]);
如果你想根据 id 的降序来筛选结果,可以使用 lazyByIdDesc 方法。
游标
与 lazy 方法类似,cursor 方法可在遍历成千上万条 Eloquent 模型记录时,大幅降低应用的内存消耗。
cursor 方法只会执行一次数据库查询;但每个 Eloquent 模型直到实际迭代时才会被实例化。因此,在迭代 cursor 时,内存中始终只会存在一个 Eloquent 模型。
由于 cursor 方法每次只在内存中保留一个模型,它无法进行关系的预加载(eager load)。如果需要预加载关系,请考虑使用 lazy 方法。
在内部,cursor 方法使用 PHP 的生成器(generator)实现此功能:
use App\Models\Flight;
foreach (Flight::where('destination', 'Zurich')->cursor() as $flight) {
// ...
}
cursor 返回一个 Illuminate\Support\LazyCollection 实例。Lazy Collection 允许你在每次只加载单个模型的情况下,使用 Laravel 集合提供的许多方法:
use App\Models\User;
$users = User::cursor()->filter(function (User $user) {
return $user->id > 500;
});
foreach ($users as $user) {
echo $user->id;
}
尽管 cursor 方法比普通查询消耗的内存少得多(每次只在内存中保留一个模型),但最终仍可能耗尽内存。这是因为 PHP 的 PDO 驱动会在内部缓冲区缓存所有原始查询结果。如果你需要处理非常大量的 Eloquent 记录,建议使用 lazy 方法。
高级子查询
子查询选择(Subquery Selects)
Eloquent 还支持高级子查询功能,使你能够在单条查询中从关联表获取信息。例如,假设我们有一个航班目的地表(destinations)和一个航班表(flights),航班表包含一个 arrived_at 列,用于表示航班到达目的地的时间。
利用查询构建器的 select 和 addSelect 方法提供的子查询功能,我们可以在单条查询中获取所有目的地以及最近到达该目的地的航班名称:
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();
子查询排序(Subquery Ordering)
此外,查询构建器的 orderBy 方法也支持子查询。继续使用航班示例,我们可以通过子查询对所有目的地按最近航班到达时间进行排序,同样只需执行一条数据库查询:
return Destination::orderByDesc(
Flight::select('arrived_at')
->whereColumn('destination_id', 'destinations.id')
->orderByDesc('arrived_at')
->limit(1)
)->get();
检索单个模型 / 聚合
除了检索符合条件的所有记录之外,你还可以使用 find、first 或 firstWhere 方法检索单条记录。这些方法不会返回模型集合,而是返回单个模型实例:
use App\Models\Flight;
// 通过主键检索模型...
$flight = Flight::find(1);
// 检索符合查询条件的第一条模型...
$flight = Flight::where('active', 1)->first();
// 检索符合查询条件的第一条模型的另一种方式...
$flight = Flight::firstWhere('active', 1);
有时你可能希望在没有结果时执行其他操作。findOr 和 firstOr 方法会返回单个模型实例,如果没有找到结果,则执行给定的闭包。闭包返回的值将被视为方法的结果:
$flight = Flight::findOr(1, function () {
// ...
});
$flight = Flight::where('legs', '>', 3)->firstOr(function () {
// ...
});
未找到异常(Not Found Exceptions)
有时你希望在模型未找到时抛出异常,这在路由或控制器中尤其有用。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;
// 根据 name 检索航班,如果不存在则创建...
$flight = Flight::firstOrCreate([
'name' => 'London to Paris'
]);
// 根据 name 检索航班,如果不存在则使用 name、delayed 和 arrival_time 属性创建...
$flight = Flight::firstOrCreate(
['name' => 'London to Paris'],
['delayed' => 1, 'arrival_time' => '11:30']
);
// 根据 name 检索航班,如果不存在则实例化一个新的 Flight 对象...
$flight = Flight::firstOrNew([
'name' => 'London to Paris'
]);
// 根据 name 检索航班,如果不存在则实例化一个包含 name、delayed 和 arrival_time 属性的 Flight 对象...
$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\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 方法时,记录会被插入到数据库中。模型的 created_at 和 updated_at 时间戳会在调用 save 方法时自动设置,因此无需手动设置它们。
另外,你也可以使用 create 方法通过单行 PHP 语句来保存一个新模型。create 方法会返回插入后的模型实例:
use App\Models\Flight;
$flight = Flight::create([
'name' => 'London to Paris',
]);
但是,在使用 create 方法之前,你需要在模型类中指定 fillable 或 guarded 属性。这些属性是必须的,因为所有 Eloquent 模型默认都防护批量赋值(mass assignment)漏洞。有关批量赋值的更多信息,请参考批量赋值文档。
更新操作
save 方法也可用于更新已存在于数据库中的模型。要更新模型,应先检索该模型并设置希望更新的属性,然后调用模型的 save 方法。同样,updated_at 时间戳会自动更新,因此无需手动设置其值:
use App\Models\Flight;
$flight = Flight::find(1);
$flight->name = 'Paris to London';
$flight->save();
有时,你可能需要在更新现有模型的同时,如果不存在匹配的模型则创建一个新模型。类似于 firstOrCreate 方法,updateOrCreate 方法会直接持久化模型,因此无需手动调用 save 方法。
例如,如果存在一条 departure 为 Oakland 且 destination 为 San Diego 的航班记录,则会更新其 price 和 discounted 列;如果不存在,则创建一条新航班记录,其属性值由第一个数组参数与第二个数组参数合并而来:
$flight = Flight::updateOrCreate(
['departure' => 'Oakland', 'destination' => 'San Diego'],
['price' => 99, 'discounted' => 1]
);
在使用 firstOrCreate 或 updateOrCreate 方法时,你可能无法直接判断模型是新创建的还是已更新的。可以通过 wasRecentlyCreated 属性来判断模型在当前生命周期内是否被创建:
$flight = Flight::updateOrCreate(
// ...
);
if ($flight->wasRecentlyCreated) {
// 新航班记录已插入...
}
批量更新
你也可以针对符合条件的模型执行批量更新。例如,将所有 active 且目的地为 San Diego 的航班标记为延误:
Flight::where('active', 1)
->where('destination', 'San Diego')
->update(['delayed' => 1]);
update 方法接收一个列名和对应值的数组,返回受影响的行数。
需要注意的是,通过 Eloquent 执行批量更新时,saving、saved、updating 和 updated 模型事件不会被触发,因为更新操作并未实际检索模型实例。
检查属性变化
Eloquent 提供了 isDirty、isClean 和 wasChanged 方法,用于检查模型内部状态,确定属性自检索以来是否发生了变化。
isDirty方法判断模型的任意属性是否自检索以来被修改。你可以传入属性名或属性数组来判断特定属性是否被修改。isClean方法判断属性自检索以来是否保持不变。wasChanged方法判断模型在最近一次保存时是否有属性发生变化,也可传入特定属性名。
示例:
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
$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(); // 返回原始属性数组
getChanges方法返回模型最近一次保存时发生变化的属性数组,而getPrevious方法返回模型上次保存前的原始属性值:
$user = User::find(1);
$user->name; // John
$user->email; // john@example.com
$user->update([
'name' => 'Jack',
'email' => 'jack@example.com',
]);
$user->getChanges();
/*
[
'name' => 'Jack',
'email' => 'jack@example.com',
]
*/
$user->getPrevious();
/*
[
'name' => 'John',
'email' => 'john@example.com',
]
*/
批量赋值
你可以使用 create 方法通过一条 PHP 语句来“保存”一个新的模型。该方法会返回插入后的模型实例:
use App\Models\Flight;
$flight = Flight::create([
'name' => 'London to Paris',
]);
然而,在使用 create 方法之前,你需要在模型类中指定 $fillable 或 $guarded 属性。这些属性是必须的,因为所有 Eloquent 模型默认都受批量赋值(mass assignment)保护。
批量赋值漏洞会在用户通过 HTTP 请求提交意外字段,并修改数据库中不希望被修改的列时发生。例如,恶意用户可能通过 HTTP 请求发送 is_admin 参数,并通过模型的 create 方法将自己升级为管理员。
为防止这种情况,你需要明确指定哪些模型属性可以进行批量赋值。可以通过模型的 $fillable 属性来设置。比如,我们希望让 Flight 模型的 name 属性可批量赋值:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 可批量赋值的属性。
*
* @var array<int, string>
*/
protected $fillable = ['name'];
}
一旦指定了可批量赋值的属性,就可以使用 create 方法向数据库插入新记录。该方法会返回新创建的模型实例:
$flight = Flight::create(['name' => 'London to Paris']);
如果你已经有了模型实例,也可以使用 fill 方法通过属性数组填充模型:
$flight->fill(['name' => 'Amsterdam to Frankfurt']);
批量赋值与 JSON 列
当操作 JSON 列时,每个可批量赋值的 JSON 属性必须在模型的 $fillable 数组中指定。出于安全考虑,Laravel 在使用 $guarded 属性时不支持更新嵌套的 JSON 属性:
/**
* 可批量赋值的属性。
*
* @var array<int, string>
*/
protected $fillable = [
'options->enabled',
];
允许所有属性批量赋值
如果希望让模型的所有属性都可批量赋值,可以将 $guarded 属性定义为空数组。但如果选择取消保护(unguard),你需要格外小心,确保传给 Eloquent 的 fill、create 和 update 方法的数组都是手动构造的:
/**
* 不可批量赋值的属性。
*
* @var array<string>|bool
*/
protected $guarded = [];
批量赋值异常
默认情况下,未包含在 $fillable 数组中的属性在批量赋值操作中会被静默丢弃。在生产环境中,这是预期行为,但在本地开发时可能导致模型更改未生效而让人困惑。
如果你希望在尝试填充不可批量赋值的属性时抛出异常,可以调用 preventSilentlyDiscardingAttributes 方法。通常,这个方法应在应用的 AppServiceProvider 类的 boot 方法中调用:
use Illuminate\Database\Eloquent\Model;
/**
* 启动应用服务。
*/
public function boot(): void
{
Model::preventSilentlyDiscardingAttributes($this->app->isLocal());
}
更新或插入(Upserts)
Eloquent 的 upsert 方法可用于在单次、原子操作中更新或创建记录。该方法的第一个参数是要插入或更新的值数组;第二个参数列出用于唯一标识表中记录的列;第三个参数是一个数组,指定当数据库中已存在匹配记录时应更新的列。如果模型启用了时间戳,upsert 方法会自动设置 created_at 和 updated_at 时间戳:
Flight::upsert([
['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99],
['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150]
], uniqueBy: ['departure', 'destination'], update: ['price']);
upsert 方法第二个参数指定的列必须具有 “主键” 或 “唯一” 索引。此外,MariaDB 和 MySQL 数据库驱动会忽略 upsert 方法的第二个参数,而总是使用表的 “主键” 和 “唯一” 索引来检测已存在的记录。删除模型
要删除模型,可以在模型实例上调用 delete 方法:
use App\Models\Flight;
$flight = Flight::find(1);
$flight->delete();
通过主键删除已存在的模型
在上面的示例中,我们是先从数据库中检索模型,然后调用 delete 方法。不过,如果你知道模型的主键,也可以直接调用 destroy 方法删除模型,无需显式检索。此外,destroy 方法不仅可以接受单个主键,还可以接受多个主键、主键数组或主键集合:
Flight::destroy(1);
Flight::destroy(1, 2, 3);
Flight::destroy([1, 2, 3]);
Flight::destroy(collect([1, 2, 3]));
如果你使用了软删除模型,可以通过 forceDestroy 方法永久删除模型:
Flight::forceDestroy(1);
destroy 方法会单独加载每个模型并调用 delete 方法,以确保每个模型的 deleting 和 deleted 事件被正确触发。
使用查询删除模型
当然,你也可以构建 Eloquent 查询来删除符合条件的所有模型。例如,删除所有标记为非激活状态的航班。与批量更新类似,批量删除不会触发被删除模型的事件:
$deleted = Flight::where('active', 0)->delete();
如果要删除表中所有模型,可以执行没有任何条件的查询:
$deleted = Flight::query()->delete();
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 构建器提供了便捷方法来创建该列:
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 方法,该方法会将模型的 deleted_at 列设置为 null:
$flight->restore();
你也可以在查询中使用 restore 方法恢复多个模型。与其他“批量”操作类似,这不会触发被恢复模型的事件:
Flight::withTrashed()
->where('airline_id', 1)
->restore();
restore 方法同样可以在关系查询中使用:
$flight->history()->restore();
永久删除模型
有时你可能需要真正从数据库中移除模型。可以使用 forceDelete 方法永久删除软删除的模型:
$flight->forceDelete();
在构建 Eloquent 关系查询时,也可以使用 forceDelete:
$flight->history()->forceDelete();
查询软删除模型
包含软删除的模型
如前所述,软删除的模型在查询结果中会被自动排除。不过,你可以通过在查询上调用 withTrashed 方法,强制将软删除的模型包含在查询结果中:
use App\Models\Flight;
$flights = Flight::withTrashed()
->where('account_id', 1)
->get();
withTrashed 方法同样可以在构建关系查询时使用:
$flight->history()->withTrashed()->get();
仅检索软删除的模型
onlyTrashed 方法只会检索已软删除的模型:
$flights = Flight::onlyTrashed()
->where('airline_id', 1)
->get();
修剪模型
有时你可能希望定期删除那些不再需要的模型。为此,你可以在模型中加入 Illuminate\Database\Eloquent\Prunable 或 Illuminate\Database\Eloquent\MassPrunable trait。添加其中一个 trait 后,实现一个 prunable 方法,用于返回一个 Eloquent 查询构造器,以定位哪些模型已不再需要:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;
class Flight extends Model
{
use Prunable;
/**
* 获取可被清理的模型查询。
*/
public function prunable(): Builder
{
return static::where('created_at', '<=', now()->subMonth());
}
}
定义清理前的逻辑
在将模型标记为 可清理(Prunable) 后,你还可以定义一个 pruning 方法。该方法会在模型删除之前调用,非常适合用于删除与模型关联的其他资源(如本地文件):
/**
* 为清理做准备。
*/
protected function pruning(): void
{
// ...
}
配置好可清理模型之后,你需要在应用的 routes/console.php 文件中调度 model:prune Artisan 命令。你可以自由选择运行该命令的周期,例如每日运行:
use Illuminate\Support\Facades\Schedule;
Schedule::command('model:prune')->daily();
model:prune 命令会自动检测 app/Models 目录下的所有 “Prunable” 模型。如果你的模型存放在其他位置,可使用 --model 选项指定模型类:
Schedule::command('model:prune', [
'--model' => [Address::class, Flight::class],
])->daily();
如果你想在清理时排除某些模型,可使用 --except:
Schedule::command('model:prune', [
'--except' => [Address::class, Flight::class],
])->daily();
你可以通过 --pretend 选项测试可清理模型的查询。启用该选项后,model:prune 命令只会输出将要被清理的记录数量,而不会真正执行删除:
php artisan model:prune --pretend
如果匹配 prunable 查询,已软删除的模型将会被永久删除(即 forceDelete)。
批量清理(Mass Pruning)
当模型使用 Illuminate\Database\Eloquent\MassPrunable trait 时,模型会通过批量删除查询从数据库中移除。因此:
pruning方法不会被调用deleting和deleted事件不会触发- 模型不会被实际查询出来
- 清理效率更高
示例:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\MassPrunable;
class Flight extends Model
{
use MassPrunable;
/**
* 获取可被清理的模型查询。
*/
public function prunable(): Builder
{
return static::where('created_at', '<=', now()->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'
]);
查询作用域
全局作用域
**全局作用域(Global Scopes)**允许你为某个模型的所有查询添加约束。Laravel 自带的软删除功能正是利用全局作用域,让查询时自动排除已“删除”的模型。编写你自己的全局作用域,可以方便地确保某个模型的所有查询都会自动带上特定的查询条件。
生成作用域(Generating Scopes)
你可以使用 make:scope Artisan 命令生成新的全局作用域类。生成的作用域会被放置在 app/Models/Scopes 目录中:
php artisan make:scope AncientScope
编写全局作用域(Writing Global Scopes)
编写全局作用域十分简单。首先通过 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子句。
应用全局作用域(Applying Global Scopes)
你可以通过在模型上添加 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 手动注册作用域:
<?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);
}
}
在以上示例中,调用 User::all() 会执行如下 SQL:
select * from `users` where `created_at` < 0021-02-18 00:00:00
匿名全局作用域(Anonymous Global Scopes)
Eloquent 还允许你使用闭包定义全局作用域,适用于简单场景,无需独立类。
闭包作用域必须提供一个自定义作用域名称作为 addGlobalScope 的第一个参数:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected static function booted(): void
{
static::addGlobalScope('ancient', function (Builder $builder) {
$builder->where('created_at', '<', now()->subYears(2000));
});
}
}
移除全局作用域(Removing Global Scopes)
若你想在查询中移除某个全局作用域,可使用 withoutGlobalScope 方法,传入作用域的类名:
User::withoutGlobalScope(AncientScope::class)->get();
若是闭包作用域,则传入定义时的字符串名称:
User::withoutGlobalScope('ancient')->get();
你还可以移除多个甚至所有全局作用域:
// 移除全部全局作用域...
User::withoutGlobalScopes()->get();
// 移除部分全局作用域...
User::withoutGlobalScopes([
FirstScope::class, SecondScope::class
])->get();
// 移除所有作用域,但保留指定的...
User::withoutGlobalScopesExcept([
SecondScope::class,
])->get();
局部作用域
本地作用域(Local Scopes)
本地作用域允许你定义一组应用中常用的查询约束,并在需要时轻松复用。例如,你可能经常需要获取所有被视为“受欢迎”的用户。要定义本地作用域,只需在 Eloquent 方法上添加 Scope 属性。
作用域方法应始终返回同一个查询构造器实例或返回 void:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 将查询限定为仅包含“受欢迎”的用户。
*/
#[Scope]
protected function popular(Builder $query): void
{
$query->where('votes', '>', 100);
}
/**
* 将查询限定为仅包含“活跃”的用户。
*/
#[Scope]
protected function active(Builder $query): void
{
$query->where('active', 1);
}
}
使用本地作用域
定义作用域后,你可以在查询模型时直接调用作用域方法,甚至可以链式调用多个作用域:
use App\Models\User;
$users = User::popular()->active()->orderBy('created_at')->get();
当你需要通过 or 组合多个作用域时,为了获得正确的逻辑分组,可能需要借助闭包:
$users = User::popular()->orWhere(function (Builder $query) {
$query->active();
})->get();
为了避免上述写法过于繁琐,Laravel 提供了“高阶” orWhere 方法,让你无需闭包也能优雅地链式组合作用域:
$users = User::popular()->orWhere->active()->get();
动态作用域
有时你可能希望定义一个可以接受参数的作用域。只需要将额外的参数写入作用域方法的签名中即可。作用域参数应在 $query 参数之后声明:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 将查询限定为仅包含指定类型的用户。
*/
#[Scope]
protected function ofType(Builder $query, string $type): void
{
$query->where('type', $type);
}
}
定义参数后,你可以在调用作用域时传递相应的值:
$users = User::ofType('admin')->get();
待处理属性
如果你希望在作用域中使用某些属性来创建模型实例,并且这些属性与作用域的约束条件相同,那么你可以在构建作用域查询时使用 withAttributes 方法:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* 将查询限定为仅包含草稿。
*/
#[Scope]
protected function draft(Builder $query): void
{
$query->withAttributes([
'hidden' => true,
]);
}
}
withAttributes 方法会根据给定的属性向查询中添加 where 条件,同时也会在通过该作用域创建模型时,将这些属性自动填入新模型实例:
$draft = Post::draft()->create(['title' => 'In Progress']);
$draft->hidden; // true
如果你希望 withAttributes 不向查询添加 where 条件,可以将 asConditions 参数设置为 false:
$query->withAttributes([
'hidden' => true,
], asConditions: false);
比较模型
有时你可能需要判断两个模型是否为“同一个”模型。你可以使用 is 和 isNot 方法快速验证两个模型的主键、数据表以及数据库连接是否一致:
if ($post->is($anotherPost)) {
// ...
}
if ($post->isNot($anotherPost)) {
// ...
}
在 belongsTo、hasOne、morphTo 和 morphOne 等关系中,也可以使用 is 和 isNot 方法。当你想比较一个关联模型但又不希望为了获取该模型而执行查询时,这个方法尤其有用:
if ($post->author()->is($user)) {
// ...
}
事件
想要把 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 属性。
此属性用于将模型生命周期中的各个事件映射到你自定义的事件类。
每个事件类的构造函数应接收被影响的模型实例:
<?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 执行 批量更新或删除 时(mass update/delete),
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) {
// ...
});
}
}
如果需要,你还可以在注册模型事件监听器时使用 可入队的匿名事件监听器(queueable anonymous listeners)。这会让 Laravel 将该事件监听器放入队列,并在后台异步执行:
use function Illuminate\Events\queueable;
static::created(queueable(function (User $user) {
// ...
}));
观察者
定义观察者(Observers)
如果你需要为某个模型监听多个事件,可以使用 观察者(Observer) 将所有事件监听逻辑集中到一个类中。 观察者类的方法名对应你想监听的 Eloquent 事件,并且每个方法都接收受影响的模型实例作为唯一参数。
你可以通过 Artisan 命令快速创建一个新的观察者类:
php artisan make:observer UserObserver --model=User
此命令会将新观察者放到 app/Observers 目录中(如果不存在,会自动创建)。
生成的观察者类结构如下:
<?php
namespace App\Observers;
use App\Models\User;
class UserObserver
{
/**
* 处理 User 模型 “created” 事件。
*/
public function created(User $user): void
{
// ...
}
/**
* 处理 User 模型 “updated” 事件。
*/
public function updated(User $user): void
{
// ...
}
/**
* 处理 User 模型 “deleted” 事件。
*/
public function deleted(User $user): void
{
// ...
}
/**
* 处理 User 模型 “restored” 事件。
*/
public function restored(User $user): void
{
// ...
}
/**
* 处理 User 模型 “forceDeleted” 事件。
*/
public function forceDeleted(User $user): void
{
// ...
}
}
注册观察者
你可以在模型上通过 ObservedBy 属性来注册观察者:
use App\Observers\UserObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
#[ObservedBy([UserObserver::class])]
class User extends Authenticatable
{
//
}
或者,你可以手动注册观察者 —— 通常在应用的 AppServiceProvider 中完成:
use App\Models\User;
use App\Observers\UserObserver;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
User::observe(UserObserver::class);
}
观察者还可以监听其他事件,如 saving、retrieved 等,更多事件可以查看 Laravel 事件文档。
观察者与数据库事务
当模型在数据库事务中被创建时,你可能希望观察者 只有在事务提交后 才执行其事件处理程序。
要实现这一点,只需让观察者实现 ShouldHandleEventsAfterCommit 接口。
如果当前没有事务进行,事件将立即执行:
<?php
namespace App\Observers;
use App\Models\User;
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;
class UserObserver implements ShouldHandleEventsAfterCommit
{
/**
* 处理 User 模型 “created” 事件。
*/
public function created(User $user): void
{
// ...
}
}
禁用事件
临时 “静默” 模型事件
有时,你可能需要暂时屏蔽某个模型触发的所有事件。你可以使用 withoutEvents 方法实现这一点。
withoutEvents 方法只接受一个闭包作为参数。闭包中的所有代码在执行时都不会触发任何模型事件,并且闭包的返回值也会作为 withoutEvents 方法的返回结果:
use App\Models\User;
$user = User::withoutEvents(function () {
User::findOrFail(1)->delete();
return User::find(2);
});
保存单个模型但不触发事件
有时,你可能希望在 不触发任何事件 的情况下保存某个模型。你可以通过 saveQuietly 方法实现:
$user = User::findOrFail(1);
$user->name = 'Victoria Faith';
$user->saveQuietly();
不触发事件地执行更新、删除、软删除、恢复、复制
你同样可以使用 “Quietly” 系列方法在不触发事件的情况下执行其他模型操作,例如:
$user->deleteQuietly();
$user->forceDeleteQuietly();
$user->restoreQuietly();
(还包括 updateQuietly()、replicateQuietly() 等方法。)