快速入门

未匹配的标注
本文档最新版为 10.x,旧版本可能放弃维护,推荐阅读最新版!

Eloquent: 入门

简介

Laravel 包含了 Eloquent,这是一个对象关系映射器(ORM),使与数据库的交互变得很愉快。使用Eloquent时,每个数据库表都有一个对应的「模型」,用于与该表进行交互。除了从数据库表中检索记录外,Eloquent 模型还允许您从表中插入,更新和删除记录。

技巧:在开始之前,请确保在应用程序的 config/database.php 配置文件中配置数据库连接。有关配置数据库的更多信息,请参阅 数据库配置文档

生成模型类

首先,让我们创建一个Eloquent模型。模型通常位于app\Models目录中,并扩展Illuminate\Database\Eloquent\Model类。您可以使用make:modelArtisan command生成新模型:

php artisan make:model Flight

如果想要在生成模型的同时生成 数据库迁移,可以使用--migration-m 选项。

php artisan make:model Flight --migration

在生成模型的同时,你可能还想要各种其他类型的类,例如模型工厂、数据填充和控制器。这些选项可以组合在一起从而一次创建多个类:

# 生成模型和 Flight工厂类...
php artisan make:model Flight --factory
php artisan make:model Flight -f

# 生成模型和 Flight 数据填充类...
php artisan make:model Flight --seed
php artisan make:model Flight -s

# 生成模型和 Flight 控制器类...
php artisan make:model Flight --controller
php artisan make:model Flight -c

# 生成模型和迁移(m)、工厂(f)、数据填充(s)、控制器(c)...
php artisan make:model Flight -mfsc

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 。如果有必要,你可以定义一个protected 属性的字段 $primaryKey,用来指定为模型的主键。

    <?php

    namespace App\Models;

    use Illuminate\Database\Eloquent\Model;

    class Flight extends Model
    {
        /**
         * 与表关联的主键。
         *
         * @var string
         */
        protected $primaryKey = 'flight_id';
    }

另外,Eloquent 默认有一个 integer 值的主键,Eloquent 会自动转换这个主键为一个 integer 类型,如果你的主键不是自增或者不是数字类型,你可以在你的模型上定义一个 public 属性的 $incrementing ,并将其设置为 false

    <?php

    class Flight extends Model
    {
        /**
         * 指明模型的 ID 不是自增。
         *
         * @var bool
         */
        public $incrementing = false;
    }

如果你模型主键不是integer,你应该定义一个 protected $keyType 在你的模型上。这个属性应该有一个值 string

    <?php

    class Flight extends Model
    {
        /**
         * 自增ID的数据类型。
         *
         * @var string
         */
        protected $keyType = 'string';
    }

复合主键

Eloquent 要求每一个模型上至少有一个唯一标识 ID ,用他当做主键存储。模型是不支持“复合主键”的。无论如何,你可以添加一个多列唯一索引到你的数据表中来当做你的的唯一标识主键。

时间戳(Timestamps)

默认情况下,Eloquent希望模型相应的数据库表中存在 created_atupdated_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_ATUPDATED_AT 常量的值来实现:

<?php

class Flight extends Model
{
    const CREATED_AT = 'creation_date';
    const UPDATED_AT = 'updated_date';
}

数据库连接 (Database Connections)

默认情况下,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
{
    /**
     * The model's default values for attributes.
     *
     * @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 模型也是查询构造器,所以你也应当阅读 查询构造器 可用的所有方法。你可以在 Eloquent 查询中使用这些方法。

重新加载模型

你可以使用 freshrefresh 重新加载从数据库中检索的 Eloquent 模型实例。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 的 allget 会从数据库中取得多个结果。然而,这些方法返回的不是 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 模型当前块的数据,所以当处理大量模型数据的时候,chunk 方法将会明显减少内存的使用量:

use App\Models\Flight;

Flight::chunk(200, function ($flights) {
    foreach ($flights as $flight) {
        //
    }
});

chunk 方法的第一个参数是每一个 chunk 中数据条数。第二个参数是一个闭包函数,它在每次从数据库中查询分块的时候调用,同时,接收数据库中查询到的块结果来作为参数。

如果要根据一个字段来过滤 chunk 方法拿到的数据,同时,这个字段的数据在遍历的时候还需要更新的话,那么可以使用 chunkById 方法。在这种场景下如果使用 chunk 方法的话,得到的结果可能和预想中的不一样。在 chunkById 方法的内部,默认会查询 id 列大于前一个分块中最后一个模型的数据。

Flight::where('departed', true)
        ->chunkById(200, function ($flights) {
            $flights->each->update(['departed' => false]);
        }, $column = 'id');

游标

chunk 方法类似,在查询成千上万的 Eloquent 模型数据的时候,cursor 方法也可以用来减少应用程序的内存使用量。

cursor 方法只会执行一次数据库查询,由于单个的 Eloquent 模型在他们实际遍历完成之前不会被销毁掉,所以它在迭代完之前一直保留在内存当中。 在框架内部,cursor 方法是通过PHP的 generators去实现的:

use App\Models\Flight;

foreach (Flight::where('destination', 'Zurich')->cursor() as $flight) {
    //
}

cursor 返回一个 Illuminate\Support\LazyCollection 实例。 Lazy collections 允许你使用很多典型的Laravel集合中的可用方法,同时只向内存中加载一次模型:

use App\Models\User;

$users = User::cursor()->filter(function ($user) {
    return $user->id > 500;
});

foreach ($users as $user) {
    echo $user->id;
}

高级子查询

selects 子查询

Eloquent 也提供了高级子查询支持,你可以用单条查询语句从相关表中提取信息。举个例子, 我们想像一下我们有一个目的地表 destinations 和一个到目的地的航班表 flights. flights 表包含的 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')
    ->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();

检索单个模型 / 聚合

除了检索与给定查询匹配的所有记录之外,您还可以使用 findfirstfirstWhere 方法检索单个记录。 这些方法不返回模型集合,而是返回单个模型实例:

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 () {
    // ...
});

未找到异常

有时,如果找不到模型,您可能希望抛出异常。 这在路由或控制器中特别有用。 findOrFailfirstOrFail 方法将检索查询的第一个结果; 但是,如果没有找到结果,将会抛出一个 Illuminate\Database\Eloquent\ModelNotFoundException

$flight = Flight::findOrFail(1);

$flight = Flight::where('legs', '>', 3)->firstOrFail();

如果没有捕获异常,则会自动返回 404 响应给用户。也就是说,使用这些方法时,没有必要再写个检查来返回 404 响应:

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::firstOrNew([
    'name' => 'London to Paris'
]);

// 按名称检索航班或使用名称、延迟和到达时间属性实例化...
$flight = Flight::firstOrNew(
    ['name' => 'Tokyo to Sydney'],
    ['delayed' => 1, 'arrival_time' => '11:30']
);

检索单条唯一的模型

要检索一条表中唯一的记录,可以使用 sole() 方法,相比 first()find() ,它会返回一条表中仅有的记录,如果查询结果有重复,会抛出一个断言异常。

现有以下航班信息表

flight_no name time
001 London to Tokyo 4AM
001 London to Tokyo 4AM
002 Pairs to London 8PM
use  App\Models\Flight;  

// 检索航班号,如果重复,会抛 `MultipleRecordsFoundException` 异常,并断言重复的条数...  
$flight  =  Flight::where(['flight_no'  =>  '001'])->sole();

// 检索航班号,如果不能存在,会抛 `ModelNotFoundException` 异常...
$flight  =  Flight::where(['flight_no'  =>  '001'])->sole();

// 只有当查询条件存在与表中且是表中唯一的记录,才会返回 Model...
$flight  =  Flight::where(['flight_no'  =>  '002'])->sole();

聚合查询

在与 Eloquent 模型交互时,您还可以使用 Laravel 查询构建器 提供的 countsummax 和其他 聚合方法。 正如你所料,这些方法返回一个数值而不是一个 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_atupdated_at 时间戳会在调用 save 方法时自动设置,因此无需手动设置。

或者,您可以使用 create 方法使用单个 PHP 语句“保存”新模型。 create 方法将返回新插入的模型实例:

use App\Models\Flight;

$flight = Flight::create([
    'name' => 'London to Paris',
]);

然而,在使用 create 方法之前,你需要在你的模型类上指定一个 fillableguarded 属性。 这些属性是必需的,因为默认情况下,所有 Eloquent 模型都受到保护,免受批量赋值漏洞的影响。 要了解有关批量赋值的更多信息,请参阅 批量赋值相关文档

更新

save 方法也可以用来更新数据库中已经存在的模型。 要更新模型,您应该检索它并设置您希望更新的任何属性。 然后调用模型的 save 方法。 同样,updated_at 时间戳会自动更新,因此无需手动设置其值:

use App\Models\Flight;

$flight = Flight::find(1);

$flight->name = 'Paris to London';

$flight->save();

批量更新

还可以批量更新与给定条件匹配的所有模型。 在此示例中,所有 activedestinationSan Diego 的航班都将被标记为延迟:

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

update 方法需要一个列和值对应的数组,表示应该更新的列。

注意:当通过 Eloquent 发布批量更新时,不会为更新的模型触发 saving, saved, updatingupdated 模型事件。 这是因为在批量更新时实际上从未检索到模型。

检查属性变更

Eloquent 提供了 isDirty, isCleanwasChanged 方法,以检查模型的内部状态并确定其属性从最初加载时如何变化。

isDirty 方法确定自加载模型以来是否已更改任何属性。 您可以传递特定的属性名称来确定特定的属性是否变脏。isClean 方法与 isDirty 相反,它也接受可选的属性参数:

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(); // Array of original attributes...

批量赋值

您可以使用 create 方法使用单个 PHP 语句“保存”新模型。 插入的模型实例将通过以下方法返回给您:

use App\Models\Flight;

$flight = Flight::create([
    'name' => 'London to Paris',
]);

然而,在使用 create 方法之前,你需要在你的模型类上指定一个 fillableguarded 属性。 这些属性是必需的,因为默认情况下,所有 Eloquent 模型都受到保护,免受批量赋值漏洞的影响。

当用户传递一个意外的 HTTP 请求字段并且该字段更改了数据库中您没有预料到的列时,就会出现批量赋值漏洞。 例如,恶意用户可能会通过 HTTP 请求发送 is_admin 参数,然后将其传递给您模型的 create 方法,从而允许用户将自己升级为管理员。

所以,在开始之前,你应该定义好模型上的哪些属性是可以被批量赋值的。你可以通过模型上的 $fillable 属性来实现。 例如:让 Flight 模型的 name 属性可以被批量赋值:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name'];
}

一旦我们设置好了可以批量赋值的属性,就可以通过 create 方法插入新数据到数据库中了。 create 方法将返回保存的模型实例:

$flight = Flight::create(['name' => 'London to Paris']);

如果你已经有一个模型实例,你可以传递一个数组给 fill 方法来赋值:

$flight->fill(['name' => 'Amsterdam to Frankfurt']);

批量赋值 & JSON 列

赋值 JSON 列时,必须在模型的 $fillable 数组中指定每个列的可赋值键。为了安全起见,在使用 guarded 属性时,Laravel 不支持更新嵌套的 JSON 属性:

/**
 * 可以批量赋值的属性
 *
 * @var array
 */
protected $fillable = [
    'options->enabled',
];

允许批量赋值

如果你想让所有属性都可以批量赋值, 你可以将 $guarded 定义成一个空数组。 如果你选择解除你的模型的保护,你应该时刻特别注意传递给 Eloquent 的 fillcreateupdate 方法的数组:

/**
 * 不能被批量赋值的属性
 *
 * @var array
 */
protected $guarded = [];

新增或更新

有时,如果不存在匹配模型,您可能需要更新现有模型或创建新模型。 与 firstOrCreate 方法一样,updateOrCreate 方法将模型持久化,因此无需手动调用 save 方法。

在下面的示例中,如果存在具有 Oaklanddeparture 位置和 San Diegodestination 位置的航班,其 pricediscounted 列将被更新。 如果不存在这样的航班,将创建一个新航班,该航班具有将第一个参数数组与第二个参数数组合并后的属性:

$flight = Flight::updateOrCreate(
    ['departure' => 'Oakland', 'destination' => 'San Diego'],
    ['price' => 99, 'discounted' => 1]
);

如果您想在单个查询中执行多个新增或更新操作,那么您应该改用 upsert 方法。 该方法的第一个参数包含要插入或更新的值,而第二个参数列出唯一标识关联表中记录的列。 该方法的第三个也是最后一个参数是一个列数组,如果数据库中已经存在匹配的记录,则应该更新这些列。 如果在模型上启用了时间戳,upsert 方法将自动设置 created_atupdated_at 时间戳:

Flight::upsert([
    ['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99],
    ['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150]
], ['departure', 'destination'], ['price']);

注意:除了 SQL Server 之外的所有数据库系统都要求提供给 upsert 方法的第二个参数中的列具有主键或唯一索引。

删除模型

想删除模型,你可以调用模型实例的 delete 方法:

use App\Models\Flight;

$flight = Flight::find(1);

$flight->delete();

通过其主键删除现有模型

上面的示例中,我们先检索,再用 delete 删除。若知道主键,则用 destroy 直接删除。 destroy 可以接受一个主键、多个主键、一个主键数组或一个主键集合collection:

Flight::destroy(1);

Flight::destroy(1, 2, 3);

Flight::destroy([1, 2, 3]);

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

注意: destroy 方法会单独加载每个模型并调用 delete 方法,以正确调度 deleting and deleted 事件.

使用查询删除模型

通过Eloquent查询来删除所有符合查询条件的模型。 例如, 我们将删除所有标记为无效的航班。像批量更新一样,批量删除将不会为已删除的模型调度模型事件:

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

注意:通过Eloquent执行批量删除时,不会调度 deletingdeleted 模型事件。这是因为在执行delete语句时,不会检索而是直接删除。.

软删除

除了实际删除记录, 还可以“软删除”。 软删除不会从数据库中删除,而是在 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数据迁移 有创建这个字段的方法:

use Illuminate\Database\Schema\Blueprint;
use Illuminate\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();

类似 withTrashed 方法,restore 方法也用在 关联上:

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

永久删除

有时你可能需要从数据库中真正删除模型。要从数据库中永久删除软删除的模型,请使用 forceDelete 方法:

$flight->forceDelete();

您也可以在模型关联上调用 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();

复制模型

您可以使用 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();

查询作用域

全局作用域

全局作用域可以给模型的查询都添加上约束。Laravel 的 软删除 功能就是利用此特性从数据库中获取 「未删除」的模型。你可以编写你自己的全局作用域,很简单、方便的为每个模型查询都加上约束条件:

编写全局作用域

编写全局作用域很简单。 首先,定义一个实现接口 Illuminate\Database\Eloquent\Scope 的类。 Laravel 没有放置作用域类的常规位置,因此您可以随意将此类放置在您希望的任何目录中。

编写全局作用域很简单。定义一个实现 Illuminate\Database\Eloquent\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
{
    /**
     * Apply the scope to a given Eloquent query builder.
     *
     * @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
{
    /**
     * The "booted" method of the model.
     *
     * @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
{
    /**
     * The "booted" method of the model.
     *
     * @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 前缀。

作用域总是返回一个查询构造器实例:

<?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);
    }

    /**
     * 只查询 active 用户的作用域
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $query
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeActive($query)
    {
        return $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();

模型比较

有时可能需要判断两个模型是否「相同」。isisNot 方法可以用来快速校验两个模型是否拥有相同的主键、表和数据库连接:

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

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

当使用 belongsTohasOnemorphTomorphOne 关联 时,isisNot 方法也非常有用。 当您想比较相关模型而不发出查询来检索该模型时,此方法特别有用:

if ($post->author()->is($user)) {
    //
}

模型事件

Eloquent 模型触发几个事件,允许你挂接到模型生命周期的如下节点: retrievedcreatingcreatedupdatingupdatedsavingsaveddeletingdeletedrestoringrestoredreplicating。事件允许你每当特定模型保存或更新数据库时执行代码。每个事件通过其构造器接受模型实例。

retrieved 事件在现有模型从数据库中查找数据时触发。当新模型每一次保存时,creatingcreated 事件被触发。如果数据库中已经存在模型并且调用了 save 方法,updating / updated 事件被触发。这些情况下,saving / saved 事件也被触发。

要开始监听模型事件,请在 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;

    /**
     * The event map for the model.
     *
     * @var array
     */
    protected $dispatchesEvents = [
        'saved' => UserSaved::class,
        'deleted' => UserDeleted::class,
    ];
}

在定义和映射 Eloquent 事件后,您可以使用 事件监听器 来处理事件。

注意:通过 Eloquent 进行批量更新时,被更新模型的 savedupdated, deletingdeleted 事件不会被触发。这是因为批量更新时,并没有真的获取模型。

使用闭包

你可以注册在触发各种模型事件时执行的闭包,而不使用自定义事件类。 通常,你应该在模型的 booted 方法中注册这些闭包:

<?php

namespace App\Models;

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使用queue 执行模型事件侦听器:

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
{
    /**
     * 处理 User「created」事件
     *
     * @param  \App\Models\User  $user
     * @return void
     */
    public function created(User $user)
    {
        //
    }

    /**
     * 处理 User「updated」事件
     *
     * @param  \App\Models\User  $user
     * @return void
     */
    public function updated(User $user)
    {
        //
    }

    /**
     * 处理 User「deleted」事件
     *
     * @param  \App\Models\User  $user
     * @return void
     */
    public function deleted(User $user)
    {
        //
    }

    /**
     * 处理 User「forceDeleted」事件
     *
     * @param  \App\Models\User  $user
     * @return void
     */
    public function forceDeleted(User $user)
    {
        //
    }
}

在你希望观察的模型上使用 observe 方法注册观察者。也可以在服务提供者的 boot 方法注册观察者。下面是在 AppServiceProvider 中注册观察者的示例:

use App\Models\User;
use App\Observers\UserObserver;

/**
 * Register any events for your application.
 *
 * @return void
 */
public function boot()
{
    User::observe(UserObserver::class);
}

静默事件

也许有时候你会需要暂时将所有由模型触发的事件 “静音” 处理。那你也许可用 withoutEvents 达到目的。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();

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/laravel/8.5/elo...

译文地址:https://learnku.com/docs/laravel/8.5/elo...

上一篇 下一篇
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
贡献者:23
讨论数量: 4
发起讨论 只看当前版本


zy5666
swoole
0 个点赞 | 6 个回复 | 代码速记 | 课程版本 8.x
誓言玄夏
正文 模型检索模块层级结构与左侧树结构不一致
0 个点赞 | 4 个回复 | 问答 | 课程版本 5.8
Rxy-development
laravel8.x upsert问题
0 个点赞 | 2 个回复 | 问答 | 课程版本 8.x
adsdawa
id ii
0 个点赞 | 0 个回复 | 分享 | 课程版本 8.5