展開文件目錄

Eloquent: 入門

簡介

Laravel 包含 Eloquent,一個物件關聯映射器(ORM),使與資料庫互動變得愉快。在使用 Eloquent 時,每個資料庫表都有一個對應的「模型」,用於與該表互動。除了從資料庫表中檢索記錄外,Eloquent 模型還允許您向表中插入、更新和刪除記錄。

[!NOTE]
開始之前,請務必在應用程式的 config/database.php 組態檔中設定資料庫連線。有關如何設定資料庫的更多資訊,請查看資料庫組態文件。

產生模型類別

要開始,讓我們建立一個 Eloquent 模型。模型通常位於 app\Models 目錄中,並擴展 Illuminate\Database\Eloquent\Model 類別。您可以使用 make:model Artisan 指令 來產生新模型:

php artisan make:model Flight

如果您想在生成模型時生成資料庫遷移,您可以使用 --migration 或 -m 選項:

php artisan make:model Flight --migration

在生成模型時,您可以生成各種其他類別,例如工廠、填充器、原則、控制器和表單請求。此外,這些選項可以結合使用以一次建立多個類別:

# Generate a model and a FlightFactory class...
php artisan make:model Flight --factory
php artisan make:model Flight -f

# Generate a model and a FlightSeeder class...
php artisan make:model Flight --seed
php artisan make:model Flight -s

# Generate a model and a FlightController class...
php artisan make:model Flight --controller
php artisan make:model Flight -c

# Generate a model, FlightController resource class, and form request classes...
php artisan make:model Flight --controller --resource --requests
php artisan make:model Flight -crR

# Generate a model and a FlightPolicy class...
php artisan make:model Flight --policy

# Generate a model and a migration, factory, seeder, and controller...
php artisan make:model Flight -mfsc

# Shortcut to generate a model, migration, factory, seeder, policy, controller, and form requests...
php artisan make:model Flight --all
php artisan make:model Flight -a

# Generate 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 將假設 Flight 模型將記錄存儲在 flights 資料表中,而 AirTrafficController 模型將在 air_traffic_controllers 資料表中存儲記錄。

如果您的模型對應的資料庫表不符合這個慣例,您可以通過在模型上定義 table 屬性來手動指定模型的表名稱:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    /**
     * The table associated with the 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 假設主鍵是一個遞增的整數值,這意味著 Eloquent 會自動將主鍵轉換為整數。如果您希望使用非遞增或非數字主鍵,您必須在模型上定義一個公共 $incrementing 屬性,並將其設置為 false:

<?php

class Flight extends Model
{
    /**
     * Indicates if the model's ID is auto-incrementing.
     *
     * @var bool
     */
    public $incrementing = false;
}

如果您的模型主鍵不是整數,您應該在模型上定義一個受保護的 $keyType 屬性。此屬性的值應該是 string:

<?php

class Flight extends Model
{
    /**
     * The data type of the primary key ID.
     *
     * @var string
     */
    protected $keyType = 'string';
}

"複合" 主鍵

Eloquent 要求每個模型至少有一個可以作為其主鍵的唯一識別“ID”。Eloquent 模型不支持“複合”主鍵。但是,您可以自由地向資料庫表中添加額外的多列唯一索引,除了表的唯一識別主鍵之外。

UUID 和 ULID 主鍵

您可以選擇使用 UUID 作為 Eloquent 模型的主鍵,而不是使用自動遞增的整數。UUID 是通用唯一的字母數字識別符,長度為 36 個字符。

如果您希望模型使用 UUID 主鍵而不是自動遞增的整數主鍵,您可以在模型上使用 Illuminate\Database\Eloquent\Concerns\HasUuids 特性。當然,您應該確保模型具有 UUID 等效的主鍵列:

默認情況下,HasUuids 特性將為您的模型生成 "有序" UUID。這些 UUID 對於索引的數據庫存儲更有效,因為它們可以按字典順序排序。

您可以通過在模型上定義 newUniqueId 方法來覆蓋給定模型的 UUID 生成過程。此外,您可以通過在模型上定義 uniqueIds 方法來指定應該接收 UUID 的列:

如果您希望,您可以選擇使用 "ULIDs" 而不是 UUID。ULIDs 類似於 UUID;但是,它們只有 26 個字符長。與有序 UUID 一樣,ULIDs 可以按字典順序排序,以實現有效的數據庫索引。要使用 ULIDs,您應該在模型上使用 Illuminate\Database\Eloquent\Concerns\HasUlids 特性。您還應確保模型具有 ULID 等效的主鍵列:

時間戳記

默認情況下,Eloquent 預期您的模型對應的數據庫表上存在 created_at 和 updated_at 列。當創建或更新模型時,Eloquent 將自動設置這些列的值。如果您不希望這些列由 Eloquent 自動管理,您應該在模型上定義一個 $timestamps 屬性,其值為 false:

如果您需要自定義模型時間戳記的格式,請在模型上設置 $dateFormat 屬性。此屬性確定日期屬性在數據庫中的存儲方式,以及在將模型序列化為數組或 JSON 時的格式:

如果您需要自定義用於存儲時間戳記的列的名稱,您可以在模型上定義 CREATED_AT 和 UPDATED_AT 常量:

如果您希望在不修改模型的 updated_at 時間戳記的情況下執行模型操作,您可以在給定給 withoutTimestamps 方法的閉包中對模型進行操作:

Model::withoutTimestamps(fn () => $post->increment('reads'));

資料庫連線

預設情況下,所有 Eloquent 模型將使用為您的應用程式配置的預設資料庫連線。如果您想要指定與特定模型互動時應使用的不同連線,您應在模型上定義一個 $connection 屬性:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    /**
     * The database connection that should be used by the model.
     *
     * @var string
     */
    protected $connection = 'mysql';
}

預設屬性值

預設情況下,新實例化的模型實例將不包含任何屬性值。如果您想要為模型的某些屬性定義預設值,您可以在模型上定義一個 $attributes 屬性。放置在 $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 = [
        'options' => '[]',
        'delayed' => false,
    ];
}

配置 Eloquent 嚴格性

Laravel 提供了幾種方法,讓您可以在各種情況下配置 Eloquent 的行為和「嚴格性」。

首先,preventLazyLoading 方法接受一個可選的布林引數,指示是否應該防止延遲載入。例如,您可能希望僅在非正式環境中禁用延遲載入,以便您的正式環境將繼續正常運作,即使在正式代碼中意外存在延遲載入的關聯。通常,此方法應在應用程式的 AppServiceProvider 的 boot 方法中調用:

use Illuminate\Database\Eloquent\Model;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Model::preventLazyLoading(! $this->app->isProduction());
}

此外,您可以指示 Laravel 在嘗試填充不可填充屬性時拋出異常,方法是調用 preventSilentlyDiscardingAttributes 方法。這可以幫助防止在本地開發期間嘗試設置未添加到模型的 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')
    ->take(10)
    ->get();

[!NOTE]
由於 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的Collection類別擴展了Laravel的基礎Illuminate\Support\Collection類別,該類別提供了各種有用的方法 來與資料集合進行交互。例如,reject 方法可用於根據調用閉包的結果從集合中刪除模型:

$flights = Flight::where('destination', 'Paris')->get();

$flights = $flights->reject(function (Flight $flight) {
    return $flight->cancelled;
});

除了Laravel基礎集合類別提供的方法外,Eloquent集合類別還提供了一些額外的方法 ,專門用於與Eloquent模型集合進行交互。

由於所有Laravel的集合都實現了PHP的可迭代接口,您可以像處理數組一樣遍歷集合:

foreach ($flights as $flight) {
    echo $flight->name;
}

分塊結果

如果您嘗試通過all或get方法加載數以萬計的Eloquent記錄,您的應用程序可能會耗盡內存。您可以使用chunk方法來更有效地處理大量模型。

chunk方法將檢索一個Eloquent模型的子集,將它們傳遞給一個閉包進行處理。由於一次只檢索一個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 方法不會將每個分塊直接傳遞給回調函式,而是返回一個扁平化的 LazyCollection Eloquent 模型,這使您可以將結果作為單個流進行交互:

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]);

您可以使用 lazyByIdDesc 方法根據 id 的降序順序篩選結果。

游標

與 lazy 方法類似,cursor 方法可用於在迭代數萬個 Eloquent 模型記錄時顯著減少應用程序的內存消耗。

cursor 方法將僅執行單個數據庫查詢;但是,直到實際迭代它們時,個別的 Eloquent 模型才會被填充。因此,在迭代游標時,內存中只保留一個 Eloquent 模型。

[!WARNING]
由於 cursor 方法始終只在內存中保存單個 Eloquent 模型,它無法急於加載關係。如果您需要急於加載關係,請考慮改用 lazy 方法。

在內部,cursor 方法使用 PHP generators 來實現此功能:

use App\Models\Flight;

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

cursor 返回一個 Illuminate\Support\LazyCollection 實例。延遲集合 允許您使用許多 Laravel 典型集合上可用的集合方法,同時一次只將單個模型加載到內存中:

use App\Models\User;

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

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

雖然 cursor 方法使用的內存比常規查詢少得多(一次只在內存中保存一個 Eloquent 模型),但最終仍會耗盡內存。這是因為 PHP 的 PDO 驅動程式在內部將所有原始查詢結果緩存到其緩衝區中。如果您處理大量的 Eloquent 記錄,請考慮改用 lazy 方法。

高級子查詢

子查詢選擇

Eloquent 還提供了高級子查詢支持,允許您在單個查詢中從相關表中提取信息。例如,假設我們有一個航班 destinations 表和一個到達目的地的 flights 表。flights 表包含一個 arrived_at 列,該列指示飛行何時到達目的地。

使用查詢構建器的 select 和 addSelect 方法提供的子查詢功能,我們可以選擇所有 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();

檢索單一模型 / 聚合

除了檢索符合特定查詢的所有記錄之外,您還可以使用 find、first 或 firstWhere 方法檢索單一記錄。這些方法不會返回模型集合,而是返回單一模型實例:

use App\Models\Flight;

// Retrieve a model by its primary key...
$flight = Flight::find(1);

// Retrieve the first model matching the query constraints...
$flight = Flight::where('active', 1)->first();

// Alternative to retrieving the first model matching the query constraints...
$flight = Flight::firstWhere('active', 1);

有時,如果找不到結果,您可能希望執行其他操作。findOr 和 firstOr 方法將返回單一模型實例,或者如果找不到結果,則執行給定的閉包。閉包返回的值將被視為方法的結果:

$flight = Flight::findOr(1, function () {
    // ...
});

$flight = Flight::where('legs', '>', 3)->firstOr(function () {
    // ...
});

找不到例外

有時,如果找不到模型,您可能希望拋出異常。這在路由或控制器中特別有用。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;

// Retrieve flight by name or create it if it doesn't exist...
$flight = Flight::firstOrCreate([
    'name' => 'London to Paris'
]);

// Retrieve flight by name or create it with the name, delayed, and arrival_time attributes...
$flight = Flight::firstOrCreate(
    ['name' => 'London to Paris'],
    ['delayed' => 1, 'arrival_time' => '11:30']
);

// Retrieve flight by name or instantiate a new Flight instance...
$flight = Flight::firstOrNew([
    'name' => 'London to Paris'
]);

// Retrieve flight by name or instantiate with the name, delayed, and arrival_time attributes...
$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\Http\Controllers\Controller;
use App\Models\Flight;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

class FlightController extends Controller
{
    /**
     * Store a new flight in the database.
     */
    public function store(Request $request): RedirectResponse
    {
        // Validate the request...

        $flight = new Flight;

        $flight->name = $request->name;

        $flight->save();

        return redirect('/flights');
    }
}

在這個例子中,我們將來自傳入 HTTP 請求的 name 欄位分配給 App\Models\Flight 模型實例的 name 屬性。當我們調用 save 方法時,將在數據庫中插入一條記錄。當調用 save 方法時,模型的 created_at 和 updated_at 時間戳將自動設置,因此無需手動設置它們。

或者,您可以使用 create 方法使用單個 PHP 陳述式“保存”新模型。插入的模型實例將由 create 方法返回給您:

use App\Models\Flight;

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

但是,在使用 create 方法之前,您需要在模型類上指定 fillable 或 guarded 屬性。這些屬性是必需的,因為所有 Eloquent 模型默認受到大量分配漏洞的保護。要了解有關大量分配的更多信息,請參考大量分配文件。

更新

save 方法也可用於更新已存在於數據庫中的模型。要更新模型,您應該檢索它並設置您希望更新的任何屬性。然後,您應該調用模型的 save 方法。同樣,updated_at 時間戳將自動更新,因此無需手動設置其值:

偶爾,您可能需要更新現有模型或者在沒有匹配模型存在時創建新模型。就像firstOrCreate方法一樣,updateOrCreate方法會持久化模型,因此無需手動調用save方法。

在下面的示例中,如果存在一個departure位置為Oakland且destination位置為San Diego的航班,則其price和discounted列將被更新。如果不存在這樣的航班,將創建一個新的航班,其屬性是將第一個參數數組與第二個參數數組合併後的結果:

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

大量更新

也可以針對符合給定查詢條件的模型執行更新。在此示例中,所有active且destination為San Diego的航班將被標記為延遲:

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

update方法期望一個包含列和值對的數組,表示應該更新的列。update方法返回受影響的行數。

[!WARNING]
通過Eloquent進行大量更新時,更新的模型不會觸發saving、saved、updating和updated模型事件。這是因為在執行大量更新時實際上從未檢索模型。

檢查屬性變更

Eloquent提供了isDirty、isClean和wasChanged方法來檢查模型的內部狀態,並確定其屬性與模型最初檢索時的變化。

isDirty方法確定自從檢索模型以來是否已更改模型的任何屬性。您可以將特定屬性名稱或屬性數組傳遞給isDirty方法,以確定任何屬性是否“dirty”。isClean方法將確定自從檢索模型以來屬性是否保持不變。此方法還接受一個可選的屬性參數:

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

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

大量賦值

您可以使用 create 方法使用單個 PHP 陳述式“保存”新模型。插入的模型實例將通過該方法返回給您:

use App\Models\Flight;

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

但是,在使用 create 方法之前,您需要在模型類上指定 fillable 或 guarded 屬性之一。這些屬性是必需的,因為所有 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<int, string>
     */
    protected $fillable = ['name'];
}

一旦指定了可大量賦值的屬性,您可以使用 create 方法將新記錄插入到數據庫中。create 方法將返回新創建的模型實例:

$flight = Flight::create(['name' => '倫敦到巴黎']);

如果您已經有一個模型實例,您可以使用 fill 方法將其填充為一組屬性:

$flight->fill(['name' => '阿姆斯特丹到法蘭克福']);

大量指派和 JSON 欄位

當指派 JSON 欄位時,必須在您的模型的 $fillable 陣列中指定每個欄位的大量指派鍵。出於安全考慮,Laravel 不支援在使用 guarded 屬性時更新巢狀 JSON 屬性:

/**
 * The attributes that are mass assignable.
 *
 * @var array<int, string>
 */
protected $fillable = [
    'options->enabled',
];

允許大量指派

如果您希望使所有屬性都可以大量指派,您可以將模型的 $guarded 屬性定義為一個空陣列。如果您選擇取消保護您的模型,您應特別小心地手工製作傳遞給 Eloquent 的 fill、create 和 update 方法的陣列:

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

大量指派例外

預設情況下,未包含在 $fillable 陣列中的屬性在執行大量指派操作時會被默默捨棄。在正式環境中,這是預期的行為;然而,在本地開發期間,這可能導致困惑,不知道為什麼模型更改沒有生效。

如果您希望,您可以通過調用 preventSilentlyDiscardingAttributes 方法來指示 Laravel 在嘗試填充不可填寫屬性時拋出異常。通常,應在應用程式的 AppServiceProvider 類的 boot 方法中調用此方法:

use Illuminate\Database\Eloquent\Model;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Model::preventSilentlyDiscardingAttributes($this->app->isLocal());
}

更新或插入

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']);

[!WARNING]
除了 SQL Server 外的所有資料庫都要求 upsert 方法的第二個引數中的欄位具有 "primary" 或 "unique" 索引。此外,MariaDB 和 MySQL 資料庫驅動程式會忽略 upsert 方法的第二個引數,並始終使用表的 "primary" 和 "unique" 索引來檢測現有記錄。

刪除模型

要刪除模型,您可以在模型實例上調用 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);

[!WARNING]
destroy 方法會逐個加載每個模型並調用 delete 方法,以便為每個模型正確分派 deleting 和 deleted 事件。

使用查詢刪除模型

當然,您可以構建一個 Eloquent 查詢來刪除符合查詢條件的所有模型。在此示例中,我們將刪除所有標記為非活動的航班。與大量更新一樣,大量刪除不會為刪除的模型分派模型事件:

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

要刪除表中的所有模型,您應該執行一個不添加任何條件的查詢:

$deleted = Flight::query()->delete();

[!WARNING]
通過 Eloquent 執行大量刪除語句時,將不會為已刪除的模型分派 deleting 和 deleted 模型事件。這是因為在執行刪除語句時實際上從未檢索模型。

軟刪除

除了實際從資料庫中刪除記錄外,Eloquent 還可以對模型進行 "軟刪除"。當模型被軟刪除時,實際上並未從資料庫中移除。相反,模型上會設置一個 deleted_at 屬性,指示模型被 "刪除" 的日期和時間。要為模型啟用軟刪除,請將 Illuminate\Database\Eloquent\SoftDeletes 特性添加到模型中:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Flight extends Model
{
    use SoftDeletes;
}

[!NOTE]
SoftDeletes 特性將自動將 deleted_at 屬性轉換為 DateTime / Carbon 實例。

您還應將 deleted_at 列添加到資料庫表中。Laravel 結構生成器 包含一個幫助方法來創建此列:

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 方法。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 特性添加到您希望定期清理的模型中。在將其中一個特性添加到模型後,實現一個 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;

    /**
     * Get the prunable model query.
     */
    public function prunable(): Builder
    {
        return static::where('created_at', '<=', now()->subMonth());
    }
}

將模型標記為 Prunable 時,您還可以在模型上定義一個 pruning 方法。此方法將在刪除模型之前調用。此方法可用於刪除與模型關聯的任何其他資源,例如存儲的文件,在模型從數據庫中永久刪除之前:

在配置了可修剪模型之後,您應該在應用程式的 routes/console.php 檔案中安排 model:prune Artisan 指令。您可以自由選擇應該運行此指令的適當間隔:

use Illuminate\Support\Facades\Schedule;

Schedule::command('model:prune')->daily();

在幕後,model:prune 指令將自動偵測應用程式 app/Models 目錄中的「可修剪」模型。如果您的模型位於不同位置,您可以使用 --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 指令來測試您的 prunable 查詢。在模擬運行時,model:prune 指令將報告實際運行時將修剪多少記錄:

php artisan model:prune --pretend

[!WARNING]
如果符合可修剪查詢,軟刪除模型將永久刪除 (forceDelete)。

大量修剪

當模型標記有 Illuminate\Database\Eloquent\MassPrunable 特性時,將使用大量刪除查詢從資料庫中刪除模型。因此,不會調用 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;

    /**
     * Get the prunable model query.
     */
    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'
]);

查詢範圍

全域範圍

全域範圍允許您對給定模型的所有查詢添加約束條件。Laravel 自己的 軟刪除 功能利用全域範圍僅從數據庫中檢索“未刪除”的模型。編寫您自己的全域範圍可以提供一種方便、簡單的方式來確保給定模型的每個查詢都接收特定的約束條件。

生成範圍

要生成一個新的全域範圍,您可以調用 make:scope Artisan 命令,該命令將把生成的範圍放在應用程式的 app/Models/Scopes 目錄中:

php artisan make:scope AncientScope

編寫全域範圍

編寫全域範圍很簡單。首先,使用 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));
    }
}

[!NOTE]
如果您的全域範圍正在將列添加到查詢的 select 子句中,您應該使用 addSelect 方法而不是 select。這將防止意外替換查詢的現有 select 子句。

應用全域範圍

要將全域範圍分配給模型,您可以簡單地在模型上放置 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 方法來手動註冊全域範圍。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);
    }
}

在上面的示例中將範圍添加到 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.
     */
    protected static function booted(): void
    {
        static::addGlobalScope('ancient', function (Builder $builder) {
            $builder->where('created_at', '<', now()->subYears(2000));
        });
    }
}

移除全域範圍

如果您想要為給定查詢刪除全域範圍,您可以使用 withoutGlobalScope 方法。此方法將全局範圍的類名作為其唯一參數:

User::withoutGlobalScope(AncientScope::class)->get();

或者,如果您使用閉包定義全域範圍,您應該傳遞您分配給全域範圍的字符串名稱:

User::withoutGlobalScope('ancient')->get();

如果您想要刪除幾個甚至所有查詢的全域範圍,您可以使用 withoutGlobalScopes 方法:

// Remove all of the global scopes...
User::withoutGlobalScopes()->get();

// Remove some of the global scopes...
User::withoutGlobalScopes([
    FirstScope::class, SecondScope::class
])->get();

區域範圍

區域範圍允許您定義常見的查詢約束集,您可以在應用程序中輕鬆重複使用。例如,您可能需要經常檢索所有被認為是「受歡迎」的使用者。要定義範圍,請在 Eloquent 模型方法前加上 scope。

範圍應始終返回相同的查詢構建器實例或 void:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Scope a query to only include popular users.
     */
    public function scopePopular(Builder $query): void
    {
        $query->where('votes', '>', 100);
    }

    /**
     * Scope a query to only include active users.
     */
    public function scopeActive(Builder $query): void
    {
        $query->where('active', 1);
    }
}

使用區域範圍

一旦定義了範圍,您可以在查詢模型時調用範圍方法。但是,在調用方法時不應包含 scope 前綴。您甚至可以對各種範圍進行鏈式調用:

use App\Models\User;

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

結合多個 Eloquent 模型範圍通過 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\Builder;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Scope a query to only include users of a given type.
     */
    public function scopeOfType(Builder $query, string $type): void
    {
        $query->where('type', $type);
    }
}

一旦預期的參數已添加到您的範圍方法簽名中,您可以在調用範圍時傳遞參數:

$users = User::ofType('admin')->get();

待定屬性

如果您想使用範圍來創建具有與用於約束範圍的屬性相同的模型,則在構建範圍查詢時,您可以使用 withAttributes 方法:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * Scope the query to only include drafts.
     */
    public function scopeDraft(Builder $query): void
    {
        $query->withAttributes([
            'hidden' => true,
        ]);
    }
}

withAttributes 方法將使用給定的屬性向查詢添加 where 條件約束,並且它還將給定的屬性添加到通過範圍創建的任何模型中:

$draft = Post::draft()->create(['title' => 'In Progress']);

$draft->hidden; // true

比較模型

有時您可能需要確定兩個模型是否"相同"。is 和 isNot 方法可用於快速驗證兩個模型是否具有相同的主鍵、表和數據庫連接:

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

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

當使用 belongsTo、hasOne、morphTo 和 morphOne 關聯時,is 和 isNot 方法也可用。當您想比較一個相關模型而不發出查詢以檢索該模型時,此方法尤其有用:

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

事件

[!NOTE]
想要直接將您的 Eloquent 事件廣播到客戶端應用程式嗎?請查看 Laravel 的 模型事件廣播。

Eloquent 模型會派送多個事件,讓您可以在模型生命週期的以下時刻進行鉤取:retrieved、creating、created、updating、updated、saving、saved、deleting、deleted、trashed、forceDeleting、forceDeleted、restoring、restored 和 replicating。

當從資料庫檢索現有模型時,retrieved 事件將被派送。當首次保存新模型時,將派送 creating 和 created 事件。當修改現有模型並調用 save 方法時,將派送 updating / updated 事件。當創建或更新模型時,即使模型的屬性未更改,也將派送 saving / saved 事件。以 -ing 結尾的事件名稱在將任何更改持久化到模型之前派送,而以 -ed 結尾的事件在將更改持久化到模型之後派送。

要開始監聽模型事件,請在您的 Eloquent 模型上定義一個 $dispatchesEvents 屬性。此屬性將將 Eloquent 模型的生命週期的各個點映射到您自己的 事件類別。每個模型事件類別應該預期通過其建構子接收受影響模型的實例:

<?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 事件後,您可以使用 事件監聽器 來處理事件。

[!WARNING]
通過 Eloquent 發出大量更新或刪除查詢時,對受影響模型不會派送 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) {
            // ...
        });
    }
}

如果需要,您可以在註冊模型事件時使用 可排隊的匿名事件監聽器。這將指示 Laravel 使用您應用程式的 佇列 在背景中執行模型事件監聽器:

use function Illuminate\Events\queueable;

static::created(queueable(function (User $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
{
    /**
     * Handle the User "created" event.
     */
    public function created(User $user): void
    {
        // ...
    }

    /**
     * Handle the User "updated" event.
     */
    public function updated(User $user): void
    {
        // ...
    }

    /**
     * Handle the User "deleted" event.
     */
    public function deleted(User $user): void
    {
        // ...
    }

    /**
     * Handle the User "restored" event.
     */
    public function restored(User $user): void
    {
        // ...
    }

    /**
     * Handle the User "forceDeleted" event.
     */
    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 類別的 boot 方法中調用模型的 observe 方法來手動註冊觀察器:

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

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    User::observe(UserObserver::class);
}

[!NOTE]
觀察器可以監聽其他事件,例如 saving 和 retrieved。這些事件在 events 文件中有描述。

觀察器和資料庫交易

當模型在資料庫交易中被建立時,您可能希望指示觀察器僅在資料庫交易提交後執行其事件處理程序。您可以通過在觀察器上實現 ShouldHandleEventsAfterCommit 介面來實現此目的。如果沒有進行資料庫交易,事件處理程序將立即執行:

<?php

namespace App\Observers;

use App\Models\User;
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;

class UserObserver implements ShouldHandleEventsAfterCommit
{
    /**
     * Handle the User "created" event.
     */
    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();

您還可以在不觸發任何事件的情況下“更新”、“刪除”、“軟刪除”、“還原”和“複製”給定的模型:

$user->deleteQuietly();
$user->forceDeleteQuietly();
$user->restoreQuietly();