Eloquent: 入门
介绍
Laravel 附带的 Eloquent ORM 提供了一种美观、简单的 ActiveRecord 实现,用于与数据库交互。每个数据库表都有一个对应的“模型”,用于与该表交互。模型允许您查询表中的数据,以及向表中插入新记录。
在开始之前,请确保在 config/database.php
中配置数据库连接。有关配置数据库的更多信息,请查看文档。
定义模型
首先,让我们创建一个 Eloquent 模型。模型通常位于 app
目录中,但您可以根据 composer.json
文件的自动加载规则将它们放置在任何位置。所有 Eloquent 模型都继承自 Illuminate\Database\Eloquent\Model
类。
创建模型实例的最简单方法是使用 make:model
Artisan 命令:
php artisan make:model User
如果您希望在生成模型时生成数据库迁移,可以使用 --migration
或 -m
选项:
php artisan make:model User --migration
php artisan make:model User -m
Eloquent 模型约定
现在,让我们看一个 Flight
模型的示例,我们将使用它来从 flights
数据库表中检索和存储信息:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
//
}
表名
注意,我们没有告诉 Eloquent Flight
模型使用哪个表。按照约定,类名的“蛇形命名法”复数形式将用作表名,除非显式指定了其他名称。因此,在这种情况下,Eloquent 将假定 Flight
模型存储在 flights
表中。您可以通过在模型上定义 table
属性来指定自定义表:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 与模型关联的表。
*
* @var string
*/
protected $table = 'my_flights';
}
主键
Eloquent 还假定每个表都有一个名为 id
的主键列。您可以定义 $primaryKey
属性来覆盖此约定。
此外,Eloquent 假定主键是递增的整数值,这意味着默认情况下主键将自动转换为 int
。如果您希望使用非递增或非数字的主键,必须将模型上的公共 $incrementing
属性设置为 false
。
时间戳
默认情况下,Eloquent 期望表中存在 created_at
和 updated_at
列。如果您不希望 Eloquent 自动管理这些列,请将模型上的 $timestamps
属性设置为 false
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 指示模型是否应使用时间戳。
*
* @var bool
*/
public $timestamps = false;
}
如果您需要自定义时间戳的格式,请设置模型上的 $dateFormat
属性。此属性确定日期属性在数据库中的存储方式,以及模型序列化为数组或 JSON 时的格式:
<?php
namespace App;
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 = 'last_update';
}
数据库连接
默认情况下,所有 Eloquent 模型将使用为应用程序配置的默认数据库连接。如果您希望为模型指定不同的连接,请使用 $connection
属性:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 模型的连接名称。
*
* @var string
*/
protected $connection = 'connection-name';
}
检索模型
一旦创建了模型和其关联的数据库表,您就可以开始从数据库中检索数据。将每个 Eloquent 模型视为一个强大的查询构建器,允许您流畅地查询与模型关联的数据库表。例如:
<?php
use App\Flight;
$flights = App\Flight::all();
foreach ($flights as $flight) {
echo $flight->name;
}
添加额外的约束
Eloquent 的 all
方法将返回模型表中的所有结果。由于每个 Eloquent 模型都充当查询构建器,您还可以向查询添加约束,然后使用 get
方法检索结果:
$flights = App\Flight::where('active', 1)
->orderBy('name', 'desc')
->take(10)
->get();
由于 Eloquent 模型是查询构建器,您应该查看查询构建器上可用的所有方法。您可以在 Eloquent 查询中使用这些方法。
集合
对于像 all
和 get
这样检索多个结果的 Eloquent 方法,将返回 Illuminate\Database\Eloquent\Collection
的实例。Collection
类提供了多种有用的方法来处理 Eloquent 结果:
$flights = $flights->reject(function ($flight) {
return $flight->cancelled;
});
当然,您也可以像数组一样简单地遍历集合:
foreach ($flights as $flight) {
echo $flight->name;
}
分块结果
如果您需要处理成千上万的 Eloquent 记录,请使用 chunk
命令。chunk
方法将检索一块 Eloquent 模型,并将其传递给给定的 Closure
进行处理。使用 chunk
方法在处理大型结果集时将节省内存:
Flight::chunk(200, function ($flights) {
foreach ($flights as $flight) {
//
}
});
传递给方法的第一个参数是您希望每个“块”接收的记录数。作为第二个参数传递的闭包将为从数据库检索到的每个块调用。将执行数据库查询以检索传递给闭包的每个块的记录。
使用游标
cursor
方法允许您使用游标遍历数据库记录,这将只执行一个查询。在处理大量数据时,cursor
方法可用于大大减少内存使用:
foreach (Flight::where('foo', 'bar')->cursor() as $flight) {
//
}
检索单个模型/聚合
当然,除了检索给定表的所有记录外,您还可以使用 find
或 first
检索单个记录。这些方法返回单个模型实例,而不是返回模型集合:
// 通过主键检索模型...
$flight = App\Flight::find(1);
// 检索符合查询约束的第一个模型...
$flight = App\Flight::where('active', 1)->first();
您还可以使用主键数组调用 find
方法,这将返回匹配记录的集合:
$flights = App\Flight::find([1, 2, 3]);
未找到异常
有时您可能希望在未找到模型时抛出异常。这在路由或控制器中特别有用。findOrFail
和 firstOrFail
方法将检索查询的第一个结果;但是,如果未找到结果,将抛出 Illuminate\Database\Eloquent\ModelNotFoundException
:
$model = App\Flight::findOrFail(1);
$model = App\Flight::where('legs', '>', 100)->firstOrFail();
如果未捕获异常,将自动向用户发送 404
HTTP 响应。使用这些方法时无需编写显式检查以返回 404
响应:
Route::get('/api/flights/{id}', function ($id) {
return App\Flight::findOrFail($id);
});
检索聚合
您还可以使用查询构建器提供的 count
、sum
、max
和其他聚合方法。这些方法返回适当的标量值,而不是完整的模型实例:
$count = App\Flight::where('active', 1)->count();
$max = App\Flight::where('active', 1)->max('price');
插入和更新模型
插入
要在数据库中创建新记录,只需创建一个新的模型实例,在模型上设置属性,然后调用 save
方法:
<?php
namespace App\Http\Controllers;
use App\Flight;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class FlightController extends Controller
{
/**
* 创建一个新的航班实例。
*
* @param Request $request
* @return Response
*/
public function store(Request $request)
{
// 验证请求...
$flight = new Flight;
$flight->name = $request->name;
$flight->save();
}
}
在此示例中,我们只是将传入 HTTP 请求中的 name
参数分配给 App\Flight
模型实例的 name
属性。当我们调用 save
方法时,将在数据库中插入一条记录。调用 save
方法时,created_at
和 updated_at
时间戳将自动设置,因此无需手动设置它们。
更新
save
方法也可用于更新数据库中已存在的模型。要更新模型,您应该检索它,设置您希望更新的任何属性,然后调用 save
方法。同样,updated_at
时间戳将自动更新,因此无需手动设置其值:
$flight = App\Flight::find(1);
$flight->name = 'New Flight Name';
$flight->save();
批量更新
更新也可以针对与给定查询匹配的任意数量的模型执行。在此示例中,所有 active
且 destination
为 San Diego
的航班将被标记为延误:
App\Flight::where('active', 1)
->where('destination', 'San Diego')
->update(['delayed' => 1]);
update
方法期望一个表示应更新的列的列和值对数组。
当通过 Eloquent 发出批量更新时,将不会为更新的模型触发 saved
和 updated
模型事件。这是因为在发出批量更新时,模型从未实际检索到。
批量赋值
您还可以使用 create
方法在一行中保存新模型。插入的模型实例将从方法返回给您。但是,在这样做之前,您需要在模型上指定 fillable
或 guarded
属性,因为所有 Eloquent 模型默认情况下都防止批量赋值。
批量赋值漏洞发生在用户通过请求传递意外的 HTTP 参数,并且该参数更改了您未预期的数据库中的列。例如,恶意用户可能会通过 HTTP 请求发送 is_admin
参数,然后将其传递到模型的 create
方法中,从而允许用户将自己提升为管理员。
因此,首先,您应该定义希望批量赋值的模型属性。您可以使用模型上的 $fillable
属性来执行此操作。例如,让我们将 Flight
模型的 name
属性设为可批量赋值:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 可批量赋值的属性。
*
* @var array
*/
protected $fillable = ['name'];
}
一旦我们将属性设为可批量赋值,我们就可以使用 create
方法在数据库中插入新记录。create
方法返回保存的模型实例:
$flight = App\Flight::create(['name' => 'Flight 10']);
如果您已经有一个模型实例,可以使用 fill
方法用属性数组填充它:
$flight->fill(['name' => 'Flight 22']);
保护属性
虽然 $fillable
充当应批量赋值的属性的“白名单”,但您也可以选择使用 $guarded
。$guarded
属性应包含您不希望批量赋值的属性数组。数组中未包含的所有其他属性将是可批量赋值的。因此,$guarded
的功能类似于“黑名单”。当然,您应该使用 $fillable
或 $guarded
- 而不是两者。在下面的示例中,除 price
外的所有属性都将是可批量赋值的:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 不可批量赋值的属性。
*
* @var array
*/
protected $guarded = ['price'];
}
如果您希望使所有属性都可批量赋值,可以将 $guarded
属性定义为空数组:
/**
* 不可批量赋值的属性。
*
* @var array
*/
protected $guarded = [];
其他创建方法
firstOrCreate
/ firstOrNew
您还可以使用其他两种方法通过批量赋值属性来创建模型:firstOrCreate
和 firstOrNew
。firstOrCreate
方法将尝试使用给定的列/值对定位数据库记录。如果在数据库中找不到模型,将使用给定的属性插入一条记录。
firstOrNew
方法与 firstOrCreate
类似,将尝试在数据库中查找匹配给定属性的记录。但是,如果未找到模型,将返回一个新的模型实例。请注意,firstOrNew
返回的模型尚未持久化到数据库。您需要手动调用 save
来持久化它:
// 通过名称检索航班,如果不存在则创建它...
$flight = App\Flight::firstOrCreate(['name' => 'Flight 10']);
// 通过名称检索航班,或使用名称和延迟属性创建它...
$flight = App\Flight::firstOrCreate(
['name' => 'Flight 10'], ['delayed' => 1]
);
// 通过名称检索,或实例化...
$flight = App\Flight::firstOrNew(['name' => 'Flight 10']);
// 通过名称检索,或使用名称和延迟属性实例化...
$flight = App\Flight::firstOrNew(
['name' => 'Flight 10'], ['delayed' => 1]
);
updateOrCreate
您可能还会遇到需要更新现有模型或在不存在时创建新模型的情况。Laravel 提供了一个 updateOrCreate
方法来一步完成此操作。与 firstOrCreate
方法一样,updateOrCreate
持久化模型,因此无需调用 save()
:
// 如果有从奥克兰到圣地亚哥的航班,将价格设置为 $99。
// 如果不存在匹配的模型,则创建一个。
$flight = App\Flight::updateOrCreate(
['departure' => 'Oakland', 'destination' => 'San Diego'],
['price' => 99]
);
删除模型
要删除模型,请在模型实例上调用 delete
方法:
$flight = App\Flight::find(1);
$flight->delete();
通过键删除现有模型
在上面的示例中,我们从数据库中检索模型,然后调用 delete
方法。但是,如果您知道模型的主键,可以在不检索模型的情况下删除模型。为此,请调用 destroy
方法:
App\Flight::destroy(1);
App\Flight::destroy([1, 2, 3]);
App\Flight::destroy(1, 2, 3);
通过查询删除模型
当然,您也可以对一组模型运行删除语句。在此示例中,我们将删除所有标记为非活动的航班。与批量更新一样,批量删除不会为删除的模型触发任何模型事件:
$deletedRows = App\Flight::where('active', 0)->delete();
当通过 Eloquent 执行批量删除语句时,将不会为删除的模型触发 deleting
和 deleted
模型事件。这是因为在执行删除语句时,模型从未实际检索到。
软删除
除了实际从数据库中删除记录外,Eloquent 还可以“软删除”模型。当模型被软删除时,它们实际上并没有从数据库中删除。相反,deleted_at
属性将在模型上设置并插入到数据库中。如果模型具有非空的 deleted_at
值,则该模型已被软删除。要启用模型的软删除,请在模型上使用 Illuminate\Database\Eloquent\SoftDeletes
trait,并将 deleted_at
列添加到 $dates
属性中:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Flight extends Model
{
use SoftDeletes;
/**
* 应转换为日期的属性。
*
* @var array
*/
protected $dates = ['deleted_at'];
}
当然,您应该将 deleted_at
列添加到数据库表中。Laravel schema builder 包含一个帮助方法来创建此列:
Schema::table('flights', function ($table) {
$table->softDeletes();
});
现在,当您在模型上调用 delete
方法时,deleted_at
列将设置为当前日期和时间。而且,当查询使用软删除的模型时,软删除的模型将自动从所有查询结果中排除。
要确定给定的模型实例是否已被软删除,请使用 trashed
方法:
if ($flight->trashed()) {
//
}
查询软删除的模型
包含软删除的模型
如上所述,软删除的模型将自动从查询结果中排除。但是,您可以使用查询上的 withTrashed
方法强制软删除的模型出现在结果集中:
$flights = App\Flight::withTrashed()
->where('account_id', 1)
->get();
withTrashed
方法也可以在关系查询上使用:
$flight->history()->withTrashed()->get();
仅检索软删除的模型
onlyTrashed
方法将仅检索软删除的模型:
$flights = App\Flight::onlyTrashed()
->where('airline_id', 1)
->get();
恢复软删除的模型
有时您可能希望“取消删除”软删除的模型。要将软删除的模型恢复到活动状态,请在模型实例上使用 restore
方法:
$flight->restore();
您还可以在查询中使用 restore
方法快速恢复多个模型。同样,与其他“批量”操作一样,这不会为恢复的模型触发任何模型事件:
App\Flight::withTrashed()
->where('airline_id', 1)
->restore();
与 withTrashed
方法一样,restore
方法也可以在关系上使用:
$flight->history()->restore();
永久删除模型
有时您可能需要真正从数据库中删除模型。要从数据库中永久删除软删除的模型,请使用 forceDelete
方法:
// 强制删除单个模型实例...
$flight->forceDelete();
// 强制删除所有相关模型...
$flight->history()->forceDelete();
查询范围
全局范围
全局范围允许您为给定模型的所有查询添加约束。Laravel 自己的软删除功能利用全局范围仅从数据库中提取“未删除”的模型。编写自己的全局范围可以提供一种方便、简单的方法来确保给定模型的每个查询都接收某些约束。
编写全局范围
编写全局范围很简单。定义一个实现 Illuminate\Database\Eloquent\Scope
接口的类。此接口要求您实现一个方法:apply
。apply
方法可以根据需要向查询添加 where
约束:
<?php
namespace App\Scopes;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
class AgeScope 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('age', '>', 200);
}
}
如果您的全局范围正在向查询的 select 子句添加列,您应该使用 addSelect
方法而不是 select
。这将防止无意中替换查询的现有 select 子句。
应用全局范围
要将全局范围分配给模型,您应该重写给定模型的 boot
方法并使用 addGlobalScope
方法:
<?php
namespace App;
use App\Scopes\AgeScope;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 模型的“启动”方法。
*
* @return void
*/
protected static function boot()
{
parent::boot();
static::addGlobalScope(new AgeScope);
}
}
添加范围后,对 User::all()
的查询将生成以下 SQL:
select * from `users` where `age` > 200
匿名全局范围
Eloquent 还允许您使用闭包定义全局范围,这对于不需要单独类的简单范围特别有用:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
class User extends Model
{
/**
* 模型的“启动”方法。
*
* @return void
*/
protected static function boot()
{
parent::boot();
static::addGlobalScope('age', function (Builder $builder) {
$builder->where('age', '>', 200);
});
}
}
移除全局范围
如果您希望为给定查询移除全局范围,可以使用 withoutGlobalScope
方法。该方法接受全局范围的类名作为其唯一参数:
User::withoutGlobalScope(AgeScope::class)->get();
如果您希望移除多个甚至所有全局范围,可以使用 withoutGlobalScopes
方法:
// 移除所有全局范围...
User::withoutGlobalScopes()->get();
// 移除一些全局范围...
User::withoutGlobalScopes([
FirstScope::class, SecondScope::class
])->get();
本地范围
本地范围允许您定义可以在整个应用程序中轻松重用的常见约束集。例如,您可能需要频繁检索所有被认为是“受欢迎”的用户。要定义范围,只需在 Eloquent 模型方法前加上 scope
。
范围应始终返回查询构建器实例:
<?php
namespace App;
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 \Illuminate\Database\Eloquent\Builder
*/
public function scopeActive($query)
{
return $query->where('active', 1);
}
}
使用本地范围
定义范围后,您可以在查询模型时调用范围方法。但是,调用方法时不需要包含 scope
前缀。您甚至可以链接调用各种范围,例如:
$users = App\User::popular()->active()->orderBy('created_at')->get();
动态范围
有时您可能希望定义一个接受参数的范围。要开始,只需将额外的参数添加到范围中。范围参数应在 $query
参数之后定义:
<?php
namespace App;
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 = App\User::ofType('admin')->get();
事件
Eloquent 模型触发多个事件,允许您在模型生命周期的以下点挂钩:creating
、created
、updating
、updated
、saving
、saved
、deleting
、deleted
、restoring
、restored
。事件允许您在每次特定模型类在数据库中保存或更新时轻松执行代码。
每当首次保存新模型时,将触发 creating
和 created
事件。如果模型已存在于数据库中并调用 save
方法,将触发 updating
/ updated
事件。但是,在这两种情况下,都会触发 saving
/ saved
事件。
要开始,请在 Eloquent 模型上定义一个 $events
属性,将 Eloquent 模型生命周期的各个点映射到您自己的事件类:
<?php
namespace App;
use App\Events\UserSaved;
use App\Events\UserDeleted;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use Notifiable;
/**
* 模型的事件映射。
*
* @var array
*/
protected $events = [
'saved' => UserSaved::class,
'deleted' => UserDeleted::class,
];
}
观察者
如果您正在监听给定模型上的许多事件,可以使用观察者将所有监听器分组到一个类中。观察者类具有反映您希望监听的 Eloquent 事件的方法名称。每个方法接收模型作为其唯一参数。Laravel 不包含观察者的默认目录,因此您可以创建任何目录来存放观察者类:
<?php
namespace App\Observers;
use App\User;
class UserObserver
{
/**
* 监听用户创建事件。
*
* @param User $user
* @return void
*/
public function created(User $user)
{
//
}
/**
* 监听用户删除事件。
*
* @param User $user
* @return void
*/
public function deleting(User $user)
{
//
}
}
要注册观察者,请在您希望观察的模型上使用 observe
方法。您可以在服务提供者的 boot
方法中注册观察者。在此示例中,我们将在 AppServiceProvider
中注册观察者:
<?php
namespace App\Providers;
use App\User;
use App\Observers\UserObserver;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* 启动任何应用程序服务。
*
* @return void
*/
public function boot()
{
User::observe(UserObserver::class);
}
/**
* 注册服务提供者。
*
* @return void
*/
public function register()
{
//
}
}