Eloquent 模型入门


简介

Laravel 内置的 Eloquent ORM 提供了一个美观、简单的与数据库打交道的 ActiveRecord 实现,每张数据表都对应一个与该表进行交互的模型(Model),通过模型类,你可以对数据表进行查询、插入、更新、删除等操作。

在开始之前,确保在 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 中的信息:

<?php
    
namespace App\Models;
    
use Illuminate\Database\Eloquent\Model;
    
class Flight extends Model
{
    //
}

表名

注意我们并没有告诉 Eloquent 我们的 Flight 模型使用哪张表,默认规则是小写的模型类名复数格式作为与其对应的表名(除非在模型类中明确指定了其它名称)。所以,在本例中,Eloquent 认为 Flight 模型存储记录在 flights 表中。你也可以在模型中定义 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
{
    /**
     * The primary key associated with the table.
     *
     * @var string
     */
    protected $primaryKey = 'flight_id';
}

此外,Eloquent 默认主键字段是自增的整型数据,这意味着主键将会被自动转化为 int 类型,如果你想要使用非自增或非数字类型主键,必须在对应模型中设置 $incrementing 属性为 false

<?php
    
class Flight extends Model
{
    /**
     * Indicates if the IDs are auto-incrementing.
     *
     * @var bool
     */
    public $incrementing = false;
}

如果主键不是整型,还要设置 $keyType 属性值为 string

<?php

class Flight extends Model
{
    /**
     * The "type" of the auto-incrementing ID.
     *
     * @var string
     */
    protected $keyType = 'string';
}

时间戳

默认情况下,Eloquent 期望 created_atupdated_at 已经存在于数据表中,如果你不想要这些 Laravel 自动管理的数据列,在模型类中设置 $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_ATUPDATED_AT 常量:

<?php
    
class Flight extends Model
{
    const CREATED_AT = 'creation_date';
    const UPDATED_AT = 'last_update';
}

数据库连接

默认情况下,所有的 Eloquent 模型使用应用配置中的默认数据库连接,如果你想要为模型指定不同的连接,可以通过 $connection 属性来设置:

<?php
    
namespace App\Models;
    
use Illuminate\Database\Eloquent\Model;
    
class Flight extends Model
{
    /**
     * The connection name for the model.
     *
     * @var string
     */
    protected $connection = 'connection-name';
}

默认属性值

如果你想要定义某些模型属性的默认值,可以在模型上定义 $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 = [
        'delayed' => false,
    ];
}

获取模型

创建完模型及其关联的数据表后,就可以从数据库中获取数据了。将 Eloquent 模型看作功能强大的查询构建器,你可以使用它来流畅地查询与其关联的数据表。例如:

<?php    
$flights = App\Models\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 查询中使用查询构建器的所有方法。

刷新模型

你可以使用 freshrefresh 方法刷新模型,fresh 方法将会从数据库中重新获取模型,已存在的模型实例不会受任何影响:

$flight = App\Models\Flight::where('number', 'FR 900')->first();
    
$freshFlight = $flight->fresh();

refresh 方法会使用数据库中获取的新数据重新水合现有模型,此外,所有加载的关联关系也会被重新刷新:

$flight = App\Models\Flight::where('number', 'FR 900')->first();
    
$flight->number = 'FR 456';
    
$flight->refresh();
    
$flight->number; // "FR 900"

集合

对 Eloquent 中获取多个结果的方法(比如 allget)而言,其返回值是 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) {
    //
}

cursor 返回的是 Illuminate\Support\LazyCollection 实例,懒集合允许你使用 Laravel 集合提供的大部分集合方法并且每次只加载一个模型到内存:

$users = App\User::cursor()->filter(function ($user) {
    return $user->id > 500;
});
    
foreach ($users as $user) {
    echo $user->id;
}

高级子查询

子查询 Select

Eloquent 还提供了高级子查询支持,该特性允许你从关联数据表中拉取信息到单个查询。例如,假设我们有一个航班目的地表 destinations 和一个飞向这些目的地的航班表 flightsflights 包含用于记录航班到达目的地时间的 arrived_at 字段。

子查询功能对 selectaddSelect 方法有效,接下来,我们可以通过一个查询获取所有的 destinations 和最近达到的航班名称:

use App\Models\Destination;
use App\Models\Flight;
    
return Destination::addSelect(['last_flight' => Flight::select('name')
    ->whereColumn('destination_id', 'destinations.id')
    ->orderBy('arrived_at', 'desc')
    ->limit(1)
])->get();

子查询排序

此外,查询构建器的 orderBy 函数也支持子查询。我们可以使用该功能基于航班最晚到达对目的地进行排序。同样,这也只需要对数据库做一次查询:

return Destination::orderByDesc(
    Flight::select('arrived_at')
        ->whereColumn('destination_id', 'destinations.id')
        ->orderBy('arrived_at', 'desc')
        ->limit(1)
)->get();

获取单个模型/聚合结果

当然,除了从给定表中获取所有记录之外,还可以使用 findfirst 获取单个记录。这些方法返回单个模型实例而不是模型集合:

// 通过主键获取模型...
$flight = App\Models\Flight::find(1);
    
// 获取匹配查询条件的第一个模型...
$flight = App\Models\Flight::where('active', 1)->first();

// 上一条查询语句的简写
$flight = App\Models\Flight::firstWhere('active', 1);

还可以通过传递主键数组来调用 find 方法,这将会返回匹配记录集合:

$flights = App\Models\Flight::find([1, 2, 3]);

有时候你可能希望获取某个查询的第一个结果或者当结果为空时执行其他动作。firstOr 方法将会返回第一个结果,或者当查询结果为空时,执行给定的回调函数,回调结果会被当作 firstOr 方法的返回结果:

$model = App\Models\Flight::where('legs', '>', 100)->firstOr(function () {
        // ...
});

firstOr 方法还可以接收要获取的字段列数组:

$model = App\Models\Flight::where('legs', '>', 100)
            ->firstOr(['id', 'legs'], function () {
                // ...
            });

Not Found 异常

有时候你可能想要在模型找不到的时候抛出异常,这在路由或控制器中非常有用,findOrFailfirstOrFail 方法会获取查询到的第一个结果。不过,如果没有任何查询结果,Illuminate\Database\Eloquent\ModelNotFoundException 异常将会被抛出:

$model = App\Models\Flight::findOrFail(1);
$model = App\Models\Flight::where('legs', '>', 100)->firstOrFail();

如果异常没有被捕获,那么 HTTP 404 响应将会被发送给用户,所以在使用这些方法的时候没有必要对返回 404 响应编写额外的检查:

Route::get('/api/flights/{id}', function ($id) {
    return App\Models\Flight::findOrFail($id);
});

获取聚合结果

当然,你还可以使用查询构建器提供的聚合方法,例如 countsummax,以及其它查询构建器提供的聚合函数。这些方法返回计算后的结果而不是整个模型实例:

$count = App\Models\Flight::where('active', 1)->count();
$max = App\Models\Flight::where('active', 1)->max('price');

插入/更新模型

插入

想要在数据库中插入新的记录,只需创建一个新的模型实例,设置模型的属性,然后调用 save 方法:

<?php
    
namespace App\Http\Controllers;
    
use App\Models\Flight;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
    
class FlightController extends Controller{
    /**
     * 创建一个新的航班实例
     *
     * @param  Request  $request
     * @return Response
     * @translator laravelacademy.org
     */
    public function store(Request $request)
    {
        // 验证请求...
    
        $flight = new Flight;
    
        $flight->name = $request->name;
    
        $flight->save();
    }
}

在这个例子中,我们只是简单分配 HTTP 请求中的 name 参数值给 App\Models\Flight 模型实例的 name 属性,当我们调用 save 方法时,一条记录将会被插入数据库。created_atupdated_at 时间戳在 save 方法被调用时会自动被设置,所以没必要手动设置它们。

更新

save 方法还可以用于更新数据库中已存在的模型。要更新一个模型,应该先获取它,设置你想要更新的属性,然后调用 save 方法。同样,updated_at 时间戳会被自动更新,所以没必要手动设置其值:

$flight = App\Models\Flight::find(1);
$flight->name = 'New Flight Name';
$flight->save();

批量更新

更新操作还可以同时修改给定查询提供的多个模型实例,在本例中,所有有效且 destination=San Diego 的航班都被标记为延迟:

App\Models\Flight::where('active', 1)
      ->where('destination', 'San Diego')
      ->update(['delayed' => 1]);

update 方法要求以数组形式传递键值对参数,代表着数据表中应该被更新的列。

注:通过 Eloquent 进行批量更新时,savedupdated 模型事件将不会在更新模型时触发。这是因为在进行批量更新时并没有从数据库获取模型。

检查属性修改

Eloquent 提供了 isDirtyisCleanwasChanged 方法来检查模型的内部状态并判断它的属性如何从初始加载状态发生改变。

isDirty 方法会判断模型从加载之后是否有任何属性发生改变,你可以传递特定属性名进行判断,isClean 方法和 isDirty 方法作用相反,也支持传入可选的属性参数:

$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 方法保存一个新的模型。该方法返回被插入的模型实例。但是,在此之前,你需要指定模型的 fillableguarded 属性,因为所有 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
     */
    protected $fillable = ['name'];
}

设置完可以被赋值的属性之后,我们就可以使用 create 方法在数据库中插入一条新的记录。create 方法返回保存后的模型实例:

$flight = App\Models\Flight::create(['name' => 'Flight 10']);

如果你已经有了一个模型实例,可以使用 fill 方法通过数组属性来填充:

 $flight->fill(['name' => 'Flight 22']);

允许批量赋值

如果你想要让所有属性都是可批量赋值的,可以将 $guarded 属性设置为空数组:

/**
 * The attributes that aren't mass assignable.
 *
 * @var array
 */
protected $guarded = [];

其它创建方法

firstOrCreate/firstOrNew

还有其它两种可以用来创建模型的方法:firstOrCreatefirstOrNewfirstOrCreate 方法先尝试通过给定列/值对在数据库中查找记录,如果没有找到的话则通过给定属性创建一个新的记录。

firstOrNew 方法和 firstOrCreate 方法一样先尝试在数据库中查找匹配的记录,如果没有找到,则返回一个新的模型实例。需要注意的是,通过 firstOrNew 方法返回的模型实例并没有持久化到数据库中,你还需要调用 save 方法手动持久化:

// 通过属性获取航班, 如果不存在则创建...
$flight = App\Models\Flight::firstOrCreate(['name' => 'Flight 10']);
    
// 通过name获取航班,如果不存在则通过name和delayed属性创建...
$flight = App\Models\Flight::firstOrCreate(
    ['name' => 'Flight 10'], 
    ['delayed' => 1, 'arrival_time' => '11:30']
);
    
// 通过属性获取航班, 如果不存在初始化一个新的实例...
$flight = App\Models\Flight::firstOrNew(['name' => 'Flight 10']);
    
// 通过name获取,如果不存在则通过name和delayed属性创建新实例...
$flight = App\Models\Flight::firstOrNew(
    ['name' => 'Flight 10'], 
    ['delayed' => 1, 'arrival_time' => '11:30']
);

updateOrCreate

与此类似的,你还会碰到如果模型已存在则更新,否则创建新模型的场景,Laravel 提供了一个 updateOrCreate 方法来一步完成。和 firstOrCreate 方法一样,updateOrCreate 方法会持久化模型,所以无需调用 save()

// 如果有从奥克兰到圣地亚哥的航班则将价格设置为 $99
// 如果没有匹配的模型则创建之
$flight = App\Models\Flight::updateOrCreate(
    ['departure' => 'Oakland', 'destination' => 'San Diego'],
    ['price' => 99, 'discounted' => 1]
);

删除模型

要删除一个模型,调用模型实例上的 delete 方法:

$flight = App\Models\Flight::find(1);
$flight->delete();

通过主键删除模型

在上面的例子中,我们在调用 delete 方法之前从数据库中获取该模型,不过,如果你知道模型的主键的话,可以调用 destroy 方法直接删除而不需要获取它。除了传递单个主键作为参数之外,destroy 方法还可以接收多个主键,主键数组以及主键集合

App\Models\Flight::destroy(1);
App\Models\Flight::destroy([1, 2, 3]);
App\Models\Flight::destroy(1, 2, 3);
App\Models\Flight::destroy(collect([1, 2, 3]));

注:destroy 方法会独立加载每个模型然后调用它们的 delete 方法,因此 deletingdeleted 事件会触发。

通过查询删除模型

当然,你还可以通过查询删除多个模型,在本例中,我们删除所有被标记为无效的航班:

$deletedRows = App\Models\Flight::where('active', 0)->delete();

注:通过 Eloquent 进行批量删除时,deletingdeleted 模型事件在删除模型时不会被触发,这是因为在进行模型删除时不会获取模型。

软删除

除了从数据库物理删除记录外,Eloquent 还可以对模型进行“软删除”。当模型被软删除后,它们并没有真的从数据库删除,而是在模型上设置一个 deleted_at 属性并插入数据库,如果模型有一个非空 deleted_at 值,那么该模型已经被软删除了。要启用模型的软删除功能,可以使用模型上的Illuminate\Database\Eloquent\SoftDeletes trait 并添加 deleted_at 列到 $dates 属性:

<?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 构建器包含一个辅助函数来创建该数据列:

public function up()
{
    Schema::table('flights', function (Blueprint $table) {
        $table->softDeletes();
    });
}

public function down()
{
    Schema::table('flights', function (Blueprint $table) {
        $table->dropSoftDeletes();
    });
}

现在,当调用模型的 delete 方法时,deleted_at 列将被设置为当前日期和时间,并且,当查询一个使用软删除的模型时,被软删除的模型将会自动从查询结果中排除。

判断给定模型实例是否被软删除,可以使用 trashed 方法:

if ($flight->trashed()) {
    //
}

查询被软删除的模型

包含软删除模型

正如上面提到的,软删除模型将会自动从查询结果中排除,不过,如果你想要软删除模型出现在查询结果中,可以使用 withTrashed 方法:

$flights = App\Models\Flight::withTrashed()
            ->where('account_id', 1)
            ->get();

withTrashed 方法也可以用于关联查询中:

$flight->history()->withTrashed()->get();

只获取软删除模型

onlyTrashed 方法只获取软删除模型:

$flights = App\Models\Flight::onlyTrashed()
            ->where('airline_id', 1)
            ->get();

恢复软删除模型

有时候你希望恢复一个被软删除的模型,可以使用 restore 方法:

$flight->restore();

你还可以在查询中使用 restore 方法来快速恢复多个模型,同样,这也不会触发任何模型事件:

App\Models\Flight::withTrashed()
        ->where('airline_id', 1)
        ->restore();

withTrashed 方法一样,restore 方法也可以用于关联查询

$flight->history()->restore();

永久删除模型

有时候你真的需要从数据库中删除一个模型,要从数据库中永久删除记录,可以使用 forceDelete 方法:

// 强制删除单个模型实例...
$flight->forceDelete();
    
// 强制删除所有关联模型...
$flight->history()->forceDelete();

复制模型

你可以通过 replicate 方法创建一个模型实例的未保存副本,这在多个模型实例共享相同属性值时非常有用:

$shipping = App\Models\Address::create([
    'type' => 'shipping',
    'line_1' => '123 Example Street',
    'city' => 'Victorville',
    'state' => 'CA',
    'postcode' => '90001',
]);

$billing = $shipping->replicate()->fill([
    'type' => 'billing'
]);

$billing->save();

查询作用域

全局作用域

全局作用域允许我们为给定模型的所有查询添加条件约束。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
     * @translator laravelacademy.org
     */
    public function apply(Builder $builder, Model $model)
    {
        return $builder->where('age', '>', 200);
    }
}

Laravel 应用默认并没有为作用域预定义文件夹,所以你可以按照自己的喜好在 app 目录下创建 Scopes 目录。

注:如果你的全局作用域需要添加列到查询的 select 子句,需要使用 addSelect 方法来替代 select,这样就可以避免已存在的 select 查询子句造成影响。

应用全局作用域

要将全局作用域应用到模型,需要重写给定模型的 boot 方法并使用 addGlobalScope 方法:

<?php
    
namespace App\Models;
    
use App\Scopes\AgeScope;
use Illuminate\Database\Eloquent\Model;
    
class User extends Model
{
    /**
     * 模型的“启动”方法.
     *
     * @return void
     */
    protected static function booted()
    {
        static::addGlobalScope(new AgeScope);
    }
}

添加作用域后,如果使用 User::all() 查询则会生成如下 SQL 语句:

select * from `users` where `age` > 200

匿名的全局作用域

Eloquent 还允许我们使用闭包定义全局作用域,这在实现简单作用域的时候特别有用,这样的话,我们就没必要定义一个单独的类了:

<?php
    
namespace App\Models;
    
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
    
class User extends Model{
    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    protected static function booted()
    {
        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 前缀。

作用域总是返回查询构建器实例:

<?php
    
namespace App\Models;
    
use Illuminate\Database\Eloquent\Model;
    
class User extends Model
{
    /**
     * 只包含活跃用户的查询作用域
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopePopular($query)
    {
        return $query->where('votes', '>', 100);
    }
    
    /**
     * 只包含激活用户的查询作用域
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeActive($query)
    {
        return $query->where('active', 1);
    }
}

使用本地作用域

作用域被定义好了之后,就可以在查询模型的时候调用作用域方法,但调用时不需要加上 scope 前缀,你甚至可以同时调用多个作用域,例如:

$users = App\Models\User::popular()->active()->orderBy('created_at')->get();

可以使用闭包回调通过 or 查询操作符联合多个 Eloquent 模型作用域:

$users = App\Models\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 = App\Models\User::ofType('admin')->get();

比较模型

有时候你可能需要确定两个模型是否是一样的,is 方法可用于快速验证两个模型是否有相同的主键、数据表、以及数据库连接:

if ($post->is($anotherPost)) {
    //
}

事件

Eloquent 模型可以触发事件,允许你在模型生命周期中的多个时间点调用如下这些方法:retrieved, creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored。事件允许你在一个指定模型类每次保存或更新的时候执行代码。每个事件都通过构造函数传入模型实例。

retrieved 事件会在从数据库中获取已存在模型时触发。当一个新模型被首次保存的时候,creatingcreated 事件会被触发。如果一个模型已经在数据库中存在并调用 save 方法,updating/updated 事件会被触发,无论是创建还是更新,saving/saved 事件都会被触发。

注:通过 Eloquent 进行批量更新或删除时,模型事件 savedupdateddeletingdeleted 不会在更新模型上触发,这是因为这些模型在进行批量更新或删除时没有真正检索过。

举个例子,在 Eloquent 模型中定义一个 $dispatchesEvents 属性来映射模型生命周期中多个时间点与对应事件类

<?php
    
namespace App\Models;
    
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;
    
    /**
     * The event map for the model.
     *
     * @var array
     */
    protected $dispatchesEvents = [
        'saved' => UserSaved::class,
        'deleted' => UserDeleted::class,
    ];
}

定义并映射模型事件后,可以使用事件监听器来处理事件。

使用闭包

除了使用自定义事件类之外,你还可以注册在模型事件发生时执行的闭包。通常,你需要在模型类的 booted 方法中注册这些闭包:

<?php

namespace App\Models;

use App\Scopes\AgeScope;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * The "booted" method of the model.
     *
     * @return void
     */
    protected static function booted()
    {
        static::created(function ($user) {
            //
        });
    }
}

如果需要的话,你可以在注册模型事件时使用队列化的匿名事件监听器,这会告知 Laravel 通过队列运行模型事件监听器:

use function Illuminate\Events\queueable;

static::created(queueable(function ($user) {
    //
}));

观察者

定义观察者

如果你在给定模型中监听多个事件,可以使用观察者来对所有监听器分组到一个类中,观察者类拥有反射你想要监听的 Eloquent 事件对应的方法名,每个方法接收模型作为唯一参数。Artisan 命令 make:observer 是创建新的观察者类的最简单的方法:

php artisan make:observer UserObserver --model=User

该命令会将新的观察者生成到 App/Observers 目录,如果这个目录不存在,Artisan 会自动创建。刚创建的观察者类代码如下:

<?php

namespace App\Observers;
    
use App\Models\User;
    
class UserObserver
{
    /**
     * Handle to the User "created" event.
     *
     * @param  \App\User  $user
     * @return void
     */
    public function created(User $user)
    {
        //
    }
    
    /**
     * Handle the User "updated" event.
     *
     * @param  \App\User  $user
     * @return void
     */
    public function updated(User $user)
    {
        //
    }
    
    /**
     * Handle the User "deleted" event.
     *
     * @param  \App\User  $user
     * @return void
     */
    public function deleted(User $user)
    {
        //
    }
    
    /**
     * Handle the User "forceDeleted" event.
     *
     * @param  \App\Models\User  $user
     * @return void
     */
    public function forceDeleted(User $user)
    {
        //
    }
}

要注册观察者,使用你想要观察模型的 observe 方法,你可以在某个服务提供者的 boot 方法中注册观察者,在本例中,我们在 AppServiceProvider 中注册观察者:

<?php
    
namespace App\Providers;
    
use App\Models\User;
use App\Observers\UserObserver;
use Illuminate\Support\ServiceProvider;
    
class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        User::observe(UserObserver::class);
    }
    
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

消除事件

你可能偶尔需要临时「消除」模型触发的所有事件,这可以通过调用 withoutEvents 方法来实现。该方法接收一个闭包作为唯一参数,闭包中执行的任何代码都不会触发模型事件。例如,下面这段示例代码会从 App\Models\User 模型实例中获取并删除记录,但不会触发任何模型事件,给定闭包返回的所有值都会通过 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();

实例教程


Vote Vote Cancel Collect Collect Cancel

<< 上一篇: Redis

>> 下一篇: 关联关系