Eloquent:快速入门
简介
Laravel 的 Eloquent ORM 为数据库提供了漂亮简洁的 ActiveRecord 实现。每张数据表都有一个对应的「模型」,用于与之交互的。模型允许您在数据表中查询数据,并将新记录插入到数据表。
首先,确保在 config/database.php
中配置了数据库连接。有关配置数据库的更多信息,可以查看 文档。
定义模型
首先,我们创建一个 Eloquent 模型。模型通常位于 app
目录中,但您可以自由放置它们,只要可以通过 composer.json
文件自动加载。所有 Eloquent 模型都继承自 Illuminate\Database\Eloquent\Model
类。
创建一个模型实例最简单的方法是使用 Artisan 命令 make:model
:
php artisan make:model Flight
如果要在生成模型时一并生成 数据库迁移,可以使用 --migration
或 -m
选项:
php artisan make:model Flight --migration
php artisan make:model Flight -m
Eloquent 模型约定
现在,我们看一个 Flight
模型示例,我们会使用它从 flights
数据表中获取并存储信息:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
//
}
数据表名称
需要注意的是,我们并没有告诉 Eloquent 为 Flight
模型使用哪张表。按照约定,除非明确指定了另一个表名,否则,会使用类名的复数形式的「蛇形命名」作为表名。因此,在此示例中,Eloquent 会假定 Flight
模型在 flights
表中存储记录。可以通过在模型上定义一个 table
属性指定自定义数据表:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 模型关联的表
*
* @var string
*/
protected $table = 'my_flights';
}
主键
Eloquent 还会假定每张表都有一个字段名为 id
的主键。可以定义一个受保护的 $primaryKey
属性覆盖此约定。
此外,Eloquent 假定主键是一个自增的整数值,意味着默认的主键会被自动转换为 int
。如果要使用一个非自增的或非数字的主键,必须在模型中将公有属性 $incrementing
设置为 false
。如果主键不是一个整数,应该在模型中将私有属性 $keyType
设置为 string
。
时间戳
默认情况下,Eloquent 认为数据表中存在 created_at
和 updated_at
字段。如果不希望 Eloquent 自动管理这些字段,可以在模型中将 $timestamps
属性设置为 false
:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 表明模型是否应该自动进行时间戳管理
*
* @var bool
*/
public $timestamps = false;
}
如果需要自定义时间戳格式,可以在模型上设置 $dateFormat
属性。此属性决定日期属性应该怎样被存储到数据库中,以及当模型被序列化为数组或 JSON 时日期的格式:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 模型日期字段的存储格式
*
* @var string
*/
protected $dateFormat = 'U';
}
如果需要自定义用于存储时间戳的字段名,可以在模型中设置 CREATED_AT
和 UPDATED_AT
常量:
class Flight extends Model
{
const CREATED_AT = 'creation_date';
const UPDATED_AT = 'last_update';
}
数据库连接
默认情况下,所有 Eloquent 模型都会使用应用配置的默认数据库连接。如果要为模型指定不同的连接,可以使用 $connection
属性:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 模型的连接名称
*
* @var string
*/
protected $connection = 'connection-name';
}
获取模型
创建模型和 对应的数据表 后,就可以开始从数据库获取数据了。可以将每个 Eloquent 模型想象成一个强大的 查询构造器,允许您流畅地查询模型关联的数据表。例如:
$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 查询中使用任何的这些方法。
刷新模型
可以使用 fresh
和 refresh
方法刷新模型。fresh
方法会从数据库重新获取模型。不影响已存在的模型实例:
$flight = App\Flight::where('number', 'FR 900')->first();
$freshFlight = $flight->fresh();
refresh
方法会从数据库中获取新的数据合并到现有模型。此外,模型加载的关联也会同样被刷新:
$flight = App\Flight::where('number', 'FR 900')->first();
$flight->number = 'FR 456';
$flight->refresh();
$flight->number; // "FR 900"
集合
像 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 模型中的「一块」,将其传递到给定闭包进行处理。当处理很大的结果集时,使用 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
方法:
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
方法时,会插入一条记录到数据库中。created_at
和 updated_at
时间戳会在调用 save
方法时自动设置,因此无需手动设置它们。
更新
save
方法可用于更新数据库中已存在的模型。要更新模型,应该先获取它,设置任何要更新的属性,然后调用 save
方法。同样,updated_at
时间戳会自动更新,因此无需手动设置其值:
$flight = App\Flight::find(1);
$flight->name = 'New Flight Name';
$flight->save();
批量更新
还可以更新与给定查询相匹配的任意数量的模型。在如下示例中,所有有效并且目的地为圣迭戈的航班都会被标记为延误:
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
属性设置为可批量赋值:
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
以外 的所有属性都可以批量赋值:
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
方法会尝试使用给定键/值对添加数据库记录。如果在数据库中找不到模型,会从第一个参数获取属性插入一条记录,还会从可选的第二个参数中获取属性。
与 firstOrCreate
一样,firstOrNew
方法会尝试添加与给定属性相匹配的记录到数据库中。不过,如果找不到模型,会返回一个新的模型实例。需要注意的是,firstOrNew
返回的模型还没有被存储到数据库中。需要手动调用 save
存储它:
// 通过名称获取航班,或者,如果不存在就创建
$flight = App\Flight::firstOrCreate(['name' => 'Flight 10']);
// 通过名称获取航班,或者,如果不存在就使用「name」和「delayed」属性创建
$flight = App\Flight::firstOrCreate(
['name' => 'Flight 10'], ['delayed' => 1]
);
// 通过名称获取,或者实例化
$flight = App\Flight::firstOrNew(['name' => 'Flight 10']);
// 通过名称获取,或者使用「name」和「delayed」属性实例化
$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
方法删除模型,而不用获取模型。除了使用单个主键作为其参数外,destroy
方法还接收多个主键,主键数组或主键集合:
App\Flight::destroy(1);
App\Flight::destroy(1, 2, 3);
App\Flight::destroy([1, 2, 3]);
App\Flight::destroy(collect([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
属性:
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::table('flights', function (Blueprint $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
条件:
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
方法:
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 也允许使用闭包定义全局作用域,这对不定义单独类的简单作用域尤其有用:
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();
或者,如果定义了使用闭包的全局作用域:
User::withoutGlobalScope('age')->get();
如果要移除多个或者甚至所有全局作用域,可以使用 withoutGlobalScopes
方法:
// 移除所有全局作用域
User::withoutGlobalScopes()->get();
// 移除某些全局作用域
User::withoutGlobalScopes([
FirstScope::class, SecondScope::class
])->get();
本地作用域
本地作用域允许定义一套通用的约束,在应用中轻松复用。例如,可能需要频繁获取所有「受欢迎的」用户。定义本地作用域,可以在 Eloquent 模型方法前加上 scope
前缀。
作用域应始终返回一个查询构造器实例:
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
参数之后定义:
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();
比较模型
有时可能需要判断两个模型是否「相同」。is
方法可用于快速验证两个模型是否有相同的主键,数据表和数据库连接:
if ($post->is($anotherPost)) {
//
}
事件
Eloquent 模型会触发多个事件,允许您在模型的生命周期中对如下的事件点添加钩子:retrieved
,creating
,created
,updating
,updated
,saving
,saved
,deleting
,deleted
,restoring
,restored
。事件允许您在模型类每次保存或更新到数据库时轻松执行相关代码。每个事件都通过其构造函数接收一个模型实例。
retrieved
事件会在从数据库获取已有模型时触发。当新模型第一次被保存时,会触发 creating
和 created
事件。如果模型在数据库中已经存在并且调用了 save
方法,那么会触发 updating
/updated
事件。但是,以上两种情况,都会触发 saving
/saved
事件。
当通过 Eloquent 进行批量更新时,不会触发被更新模型的
saved
和updated
模型事件。这是因为进行批量更新时,实际上不会获取模型。
首先,在 Eloquent 模型上定义一个 $dispatchesEvents
属性,将 Eloquent 模型的生命周期中的各种事件点映射到自己的 事件类:
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 $dispatchesEvents = [
'saved' => UserSaved::class,
'deleted' => UserDeleted::class,
];
}
定义并映射 Eloquent 事件后,就可以使用 事件监听器 处理事件了。
观察者
定义观察者
如果要监听给定类的很多事件,可以使用观察者将所有监听器放到单个类中。观察者类的方法名与到要监听的 Eloquent 事件相对应。每个方法都接收模型作为其唯一参数。创建新的观察者类最简单的方式是使用 Artisan 命令 make:observer
:
php artisan make:observer UserObserver --model=User
此命令会将新的观察者放到 App/Observers
目录中。如果该目录不存在,Artisan 会创建。新生成的观察者看起来像这样:
namespace App\Observers;
use App\User;
class UserObserver
{
/**
* 处理用户的「created」事件
*
* @param \App\User $user
* @return void
*/
public function created(User $user)
{
//
}
/**
* 处理用户的「updated」事件
*
* @param \App\User $user
* @return void
*/
public function updated(User $user)
{
//
}
/**
* 处理用户的「deleted」事件
*
* @param \App\User $user
* @return void
*/
public function deleted(User $user)
{
//
}
}
注册观察者,可以在希望观察的模型上使用 observe
方法。可以在服务提供者的 boot
方法中注册观察者。在此示例中,我们会在 AppServiceProvider
中注册观察者:
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()
{
//
}
}