Laravel 提供了大量強大的功能,有助於提升我們的開發體驗 (DX)。但是,隨著定期發布、日常工作的壓力以及大量可用功能的出現,很容易錯過一些鮮為人知的功能,而這些功能可以幫助改進我們的代碼。
本文將介紹一些我最喜歡的 Laravel 模型使用技巧。希望這些技巧能幫助你編寫更簡潔、更高效的代碼,並幫助你避免常見的陷阱。
我們將首先介紹如何發現並防止 N 1 查詢問題。
當延遲加載關聯關係時,可能會出現常見的 N 1 查詢問題,其中 N 是運行以獲取相關模型的查詢次數。
這是什麼意思呢?讓我們來看一個例子。假設我們要從數據庫中獲取所有帖子,遍歷它們,並訪問創建帖子的用戶。我們的代碼可能如下所示:
$posts = Post::all(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
儘管上面的代碼看起來不錯,但它實際上會導致 N 1 問題。假設數據庫中有 100 個帖子。在第一行,我們將運行單個查詢以獲取所有帖子。然後,在訪問 $post->user
的 foreach
循環中,這將觸發一個新查詢以獲取該帖子的用戶;導致額外 100 個查詢。這意味著我們將總共運行 101 個查詢。正如你所想像的那樣,這並不好!它會減慢應用程序的速度,並給數據庫帶來不必要的壓力。
隨著代碼變得越來越複雜,功能越來越多,除非你積極尋找這些問題,否則很難發現這些問題。
值得慶幸的是,Laravel 提供了一個方便的 Model::preventLazyLoading()
方法,你可以使用它來幫助發現和防止這些 N 1 問題。此方法將指示 Laravel 在延遲加載關係時拋出異常,因此你可以確保始終熱切加載你的關係。
要使用此方法,可以將 Model::preventLazyLoading()
方法調用添加到你的 AppProvidersAppServiceProvider
類中:
namespace App\Providers; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot(): void { Model::preventLazyLoading(); } }
現在,如果我們要運行上面的代碼來獲取每個帖子並訪問創建該帖子的用戶,我們將看到拋出 IlluminateDatabaseLazyLoadingViolationException
異常,並顯示以下消息:
<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>
要解決此問題,我們可以更新代碼,在獲取帖子時熱切加載用戶關係。我們可以使用 with
方法來實現:
$posts = Post::with('user')->get(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
上面的代碼現在將成功運行,並且只會觸發兩個查詢:一個用於獲取所有帖子,另一個用於獲取這些帖子的所有用戶。
你嘗試訪問你認為存在於模型上但不存在的字段的頻率有多高?你可能輸入錯誤了,或者你可能認為存在 full_name
字段,而實際上它被稱為 name
。
假設我們有一個 AppModelsUser
模型,具有以下字段:
id
name
email
password
created_at
updated_at
如果我們運行以下代碼會發生什麼? :
$posts = Post::all(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
假設我們在模型上沒有 full_name
訪問器,則 $name
變量將為 null
。但我們不知道這是因為 full_name
字段實際上為 null
,還是因為我們沒有從數據庫中獲取該字段,或者因為該字段不存在於模型中。正如你所想像的那樣,這可能會導致意想不到的行為,有時很難發現。
Laravel 提供了一個 Model::preventAccessingMissingAttributes()
方法,你可以使用它來幫助防止此問題。此方法將在你嘗試訪問模型當前實例上不存在的字段時指示 Laravel 拋出異常。
要啟用此功能,可以將 Model::preventAccessingMissingAttributes()
方法調用添加到你的 AppProvidersAppServiceProvider
類中:
namespace App\Providers; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot(): void { Model::preventLazyLoading(); } }
現在,如果我們要運行我們的示例代碼並嘗試訪問 AppModelsUser
模型上的 full_name
字段,我們將看到拋出 IlluminateDatabaseEloquentMissingAttributeException
異常,並顯示以下消息:
<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>
使用 preventAccessingMissingAttributes
的另一個好處是,它可以突出顯示我們嘗試讀取模型上存在的但可能未加載的字段的情況。例如,假設我們有以下代碼:
$posts = Post::with('user')->get(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
如果我們阻止訪問缺失的屬性,則會拋出以下異常:
$user = User::query()->first(); $name = $user->full_name;
這在更新現有查詢時非常有用。例如,過去,你可能只需要模型中的幾個字段。但是,你可能現在正在更新應用程序中的功能,並且需要訪問另一個字段。如果沒有啟用此方法,你可能不會意識到你正在嘗試訪問尚未加載的字段。
值得注意的是,preventAccessingMissingAttributes
方法已從 Laravel 文檔中刪除 (commit),但它仍然有效。我不確定刪除它的原因,但這是一個需要注意的問題。這可能表明它將來會被刪除。
(以下內容與原文相同,為了保持一致性,我將保留原文,不再進行改寫)
與 preventAccessingMissingAttributes
類似,Laravel 提供了一個 preventSilentlyDiscardingAttributes
方法,可以幫助防止更新模型時出現意外行為。
假設你有一個 AppModelsUser
模型類,如下所示:
$posts = Post::all(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
正如我們所看到的,name
、email
和 password
字段都是可填充字段。但是,如果我們嘗試更新模型上不存在的字段(例如 full_name
)或存在的但不可填充的字段(例如 email_verified_at
)會發生什麼? :
namespace App\Providers; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot(): void { Model::preventLazyLoading(); } }
如果我們運行上面的代碼,full_name
和 email_verified_at
字段都將被忽略,因為它們沒有被定義為可填充字段。但不會拋出錯誤,因此我們將不知道這些字段已被靜默丟棄。
正如你所預期的那樣,這可能會導致應用程序中難以發現的錯誤,特別是如果你的“更新”語句中的任何其他內容實際上都已更新。因此,我們可以使用 preventSilentlyDiscardingAttributes
方法,該方法將在你嘗試更新模型上不存在或不可填充的字段時拋出異常。
要使用此方法,可以將 Model::preventSilentlyDiscardingAttributes()
方法調用添加到你的 AppProvidersAppServiceProvider
類中:
<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>
上面的代碼將強制拋出錯誤。
現在,如果我們嘗試運行上面的示例代碼並更新用戶的 first_name
和 email_verified_at
字段,則會拋出 IlluminateDatabaseEloquentMassAssignmentException
異常,並顯示以下消息:
$posts = Post::with('user')->get(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
值得注意的是,preventSilentlyDiscardingAttributes
方法僅在你使用 fill
或 update
等方法時才會突出顯示不可填充字段。如果你手動設置每個屬性,它將不會捕獲這些錯誤。例如,讓我們來看以下代碼:
$user = User::query()->first(); $name = $user->full_name;
在上面的代碼中,full_name
字段不存在於數據庫中,因此 Laravel 不會為我們捕獲它,而是在數據庫級別捕獲它。如果你使用的是 MySQL 數據庫,你會看到這樣的錯誤:
namespace App\Providers; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot(): void { Model::preventAccessingMissingAttributes(); } }
如果你想使用我們前面提到的三種方法,你可以使用 Model::shouldBeStrict()
方法一次啟用它們。此方法將啟用 preventLazyLoading
、preventAccessingMissingAttributes
和 preventSilentlyDiscardingAttributes
設置。
要使用此方法,可以將 Model::shouldBeStrict()
方法調用添加到你的 AppProvidersAppServiceProvider
類中:
<code>属性 [full_name] 不存在或未为模型 [App\Models\User] 获取。</code>
這等同於:
$posts = Post::all(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
與 preventAccessingMissingAttributes
方法類似,shouldBeStrict
方法已從 Laravel 文檔中刪除 (commit),但仍然有效。這可能表明它將來會被刪除。
默認情況下,Laravel 模型使用自動遞增的 ID 作為其主鍵。但有時你可能更願意使用通用唯一標識符 (UUID)。
UUID 是 128 位(或 36 個字符)的字母數字字符串,可用於唯一標識資源。由於它們是如何生成的,因此它們與另一個 UUID 衝突的可能性極低。一個 UUID 示例是:1fa24c18-39fd-4ff2-8f23-74ccd08462b0
。
你可能希望將 UUID 用作模型的主鍵。或者,你可能希望保留自動遞增的 ID 來定義應用程序和數據庫中的關係,但將 UUID 用於面向公眾的 ID。使用這種方法可以通過使攻擊者更難以猜測其他資源的 ID 來增加額外的安全層。
例如,假設我們在路由中使用自動遞增的 ID。我們可能有一個用於訪問用戶的路由,如下所示:
namespace App\Providers; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot(): void { Model::preventLazyLoading(); } }
如果路由不安全,攻擊者可以循環遍歷 ID(例如 - /users/1
、/users/2
、/users/3
等),試圖訪問其他用戶的資料。而如果我們使用 UUID,則 URL 可能更像 /users/1fa24c18-39fd-4ff2-8f23-74ccd08462b0
、/users/b807d48d-0d01-47ae-8bbc-59b2acea6ed3
和 /users/ec1dde93-c67a-4f14-8464-c0d29c95425f
。正如你所想像的那樣,這些更難以猜測。
當然,僅僅使用 UUID 並不能保護你的應用程序,它們只是你可以採取的提高安全性的額外步驟。你需要確保你還使用其他安全措施,例如速率限制、身份驗證和授權檢查。
我們首先來看一下如何將主鍵更改為 UUID。
為此,我們需要確保我們的表有一個能夠存儲 UUID 的列。 Laravel 提供了一個方便的 $table->uuid
方法,我們可以在遷移中使用它。
假設我們有這個創建 comments
表的基本遷移:
<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>
正如我們在遷移中看到的,我們定義了一個 UUID 字段。默認情況下,此字段將被稱為 uuid
,但如果你願意,可以通過向 uuid
方法傳遞列名來更改它。
然後,我們需要指示 Laravel 將新的 uuid
字段用作我們的 AppModelsComment
模型的主鍵。我們還需要添加一個特性,它將允許 Laravel 為我們自動生成 UUID。我們可以通過覆蓋模型上的 $primaryKey
屬性並使用 IlluminateDatabaseEloquentConcernsHasUuids
特性來實現:
$posts = Post::with('user')->get(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
現在應該配置好模型並準備使用 UUID 作為主鍵了。來看這個示例代碼:
$posts = Post::all(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
我們可以在轉儲的模型中看到 uuid
字段已填充了 UUID。
如果你更願意將自動遞增的 ID 用於內部關係,但將 UUID 用於面向公眾的 ID,則可以向模型添加 UUID 字段。
我們假設你的表具有 id
和 uuid
字段。由於我們將使用 id
字段作為主鍵,因此我們不需要在模型上定義 $primaryKey
屬性。
我們可以覆蓋通過 IlluminateDatabaseEloquentConcernsHasUuids
特性提供的 uniqueIds
方法。此方法應返回應為其生成 UUID 的字段數組。
讓我們更新我們的 AppModelsComment
模型以包含我們稱為 uuid
的字段:
namespace App\Providers; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot(): void { Model::preventLazyLoading(); } }
現在,如果我們要轉儲一個新的 AppModelsComment
模型,我們將看到 uuid
字段已填充了 UUID:
<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>
稍後我們將在本文中介紹如何更新你的模型和路由,以便在你的路由中使用這些 UUID 作為你的面向公眾的 ID。
與在 Laravel 模型中使用 UUID 類似,有時你可能希望使用通用唯一詞典排序標識符 (ULID)。
ULID 是 128 位(或 26 個字符)的字母數字字符串,可用於唯一標識資源。一個 ULID 示例是:01J4HEAEYYVH4N2AKZ8Y1736GD
。
你可以像定義 UUID 字段一樣定義 ULID 字段。唯一的區別是,你應該使用 IlluminateDatabaseEloquentConcernsHasUlids
特性,而不是更新你的模型以使用 IlluminateDatabaseEloquentConcernsHasUuids
特性。
例如,如果我們想更新我們的 AppModelsComment
模型以使用 ULID 作為主鍵,我們可以這樣做:
$posts = Post::with('user')->get(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
你可能已經知道什麼是路由模型綁定。但以防萬一你不知道,讓我們快速回顧一下。
路由模型綁定允許你根據傳遞到 Laravel 應用程序路由的數據自動獲取模型實例。
默認情況下,Laravel 將使用模型的主鍵字段(通常是 id
字段)進行路由模型綁定。例如,你可能有一個用於顯示單個用戶信息的路由:
$user = User::query()->first(); $name = $user->full_name;
上面示例中定義的路由將嘗試查找數據庫中存在且具有提供的 ID 的用戶。例如,假設數據庫中存在 ID 為 1
的用戶。當你訪問 URL /users/1
時,Laravel 將自動從數據庫中獲取 ID 為 1
的用戶,並將其傳遞給閉包函數(或控制器)以進行操作。但是,如果數據庫中不存在具有提供的 ID 的模型,Laravel 將自動返回 404 Not Found
響應。
但是,有時你可能希望使用不同的字段(而不是主鍵)來定義如何從數據庫中檢索模型。
例如,正如我們前面提到的,你可能希望將自動遞增的 ID 用作模型的主鍵用於內部關係。但你可能希望將 UUID 用於面向公眾的 ID。在這種情況下,你可能希望使用 uuid
字段進行路由模型綁定,而不是 id
字段。
同樣,如果你正在構建博客,你可能希望根據 slug
字段而不是 id
字段來獲取你的帖子。這是因為 slug
字段比自動遞增的 ID 更易於閱讀且更利於 SEO。
如果你想定義應用於所有路由的字段,則可以通過在模型上定義 getRouteKeyName
方法來實現。此方法應返回你希望用於路由模型綁定的字段的名稱。
例如,假設我們要將 AppModelsPost
模型的所有路由模型綁定更改為使用 slug
字段而不是 id
字段。我們可以通過向我們的 Post
模型添加 getRouteKeyName
方法來實現:
$posts = Post::all(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
這意味著我們現在可以像這樣定義我們的路由:
namespace App\Providers; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot(): void { Model::preventLazyLoading(); } }
當我們訪問 URL /posts/my-first-post
時,Laravel 將自動從數據庫中獲取 slug
為 my-first-post
的帖子,並將其傳遞給閉包函數(或控制器)以進行操作。
但是,有時你可能只想更改單個路由中使用的字段。例如,你可能希望在一個路由中使用 slug
字段進行路由模型綁定,但在所有其他路由中使用 id
字段。
我們可以通過在路由定義中使用 :field
語法來實現。例如,假設我們要在一個路由中使用 slug
字段進行路由模型綁定。我們可以像這樣定義我們的路由:
<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>
這現在意味著在這個特定路由中,Laravel 將嘗試從數據庫中獲取具有提供的 slug
字段的帖子。
當你使用 AppModelsUser::all()
等方法從數據庫中獲取多個模型時,Laravel 通常會將它們放入 IlluminateDatabaseEloquentCollection
類的實例中。此類提供了許多用於處理返回的模型的有用方法。但是,有時你可能希望返回自定義集合類而不是默認集合類。
你可能出於幾個原因想要創建一個自定義集合。例如,你可能想要添加一些特定於處理該類型模型的輔助方法。或者,你可能希望將其用於改進類型安全,並確保集合只包含特定類型的模型。
Laravel 使覆蓋應返回的集合類型變得非常容易。
讓我們來看一個例子。假設我們有一個 AppModelsPost
模型,當我們從數據庫中獲取它們時,我們希望將它們返回到自定義 AppCollectionsPostCollection
類的實例中。
我們可以創建一個新的 app/Collections/PostCollection.php
文件並像這樣定義我們的自定義集合類:
$posts = Post::all(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
在上面的示例中,我們創建了一個新的 AppCollectionsPostCollection
類,它擴展了 Laravel 的 IlluminateSupportCollection
類。我們還指定了此集合將只包含 AppModelsPost
類的實例,使用 docblock。這對於幫助你的 IDE 理解集合中將包含的數據類型非常有用。
然後,我們可以更新我們的 AppModelsPost
模型以返回自定義集合類的實例,方法是覆蓋 newCollection
方法,如下所示:
namespace App\Providers; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot(): void { Model::preventLazyLoading(); } }
在這個例子中,我們獲取傳遞給 newCollection
方法的 AppModelsPost
模型數組,並返回自定義 AppCollectionsPostCollection
類的新的實例。
現在我們可以使用自定義集合類從數據庫中獲取我們的帖子,如下所示:
<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>
我在處理項目時遇到的一個常見問題是如何比較模型。這通常是在授權檢查中,當你想檢查用戶是否可以訪問資源時。
讓我們來看一些常見的陷阱,以及為什麼你可能應該避免使用它們。
你應該避免在檢查兩個模型是否相同的時候使用 ===
。這是因為 ===
檢查在比較對象時將檢查它們是否是同一個對象的實例。這意味著即使兩個模型具有相同的數據,如果它們是不同的實例,它們也不會被認為是相同的。因此,你應該避免這樣做,因為它很可能會返回 false
。
假設 AppModelsComment
模型上存在 post
關係,並且數據庫中的第一個評論屬於第一個帖子,讓我們來看一個例子:
$posts = Post::with('user')->get(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
你應該也避免在檢查兩個模型是否相同的時候使用 ==
。這是因為 ==
檢查在比較對象時將檢查它們是否是同一類的實例,以及它們是否具有相同的屬性和值。但是,這可能會導致意想不到的行為。
來看這個例子:
$user = User::query()->first(); $name = $user->full_name;
在上面的示例中,==
檢查將返回 true
,因為 $comment->post
和 $post
是同一類,並且具有相同的屬性和值。但是,如果我們更改 $post
模型中的屬性使其不同會發生什麼?
讓我們使用 select
方法,以便我們只從 posts
表中獲取 id
和 content
字段:
$posts = Post::all(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
即使 $comment->post
與 $post
是相同的模型,==
檢查也將返回 false
,因為模型具有不同的已加載屬性。正如你所想像的那樣,這可能會導致一些難以追踪的意外行為,特別是如果你已經追溯地將 select
方法添加到查詢中,並且你的測試開始失敗。
相反,我喜歡使用 Laravel 提供的 is
和 isNot
方法。這些方法將比較兩個模型,並檢查它們是否屬於同一類,是否具有相同的主鍵值,以及是否具有相同的數據庫連接。這是一種更安全的比較模型的方法,並將有助於減少意外行為的可能性。
你可以使用 is
方法來檢查兩個模型是否相同:
namespace App\Providers; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function boot(): void { Model::preventLazyLoading(); } }
同樣,你可以使用 isNot
方法來檢查兩個模型是否不同:
<code>尝试在模型 [App\Models\Post] 上延迟加载 [user],但延迟加载已禁用。</code>
whereBelongsTo
最後一個技巧更像是個人偏好,但我發現它使我的查詢更易於閱讀和理解。
在嘗試從數據庫中獲取模型時,你可能會發現自己正在編寫基於關係的過濾查詢。例如,你可能希望獲取屬於特定用戶和帖子的所有評論:
$posts = Post::with('user')->get(); foreach ($posts as $post) { // 对帖子执行某些操作... // 尝试访问帖子的用户 echo $post->user->name; }
Laravel 提供了一個 whereBelongsTo
方法,你可以使用它來使你的查詢更易於閱讀(在我看來)。使用此方法,我們可以像這樣重寫上面的查詢:
$user = User::query()->first(); $name = $user->full_name;
我喜歡這種語法糖,並且覺得它使查詢更易於閱讀。這也是確保你根據正確的關係和字段進行過濾的好方法。
你或你的團隊可能更喜歡使用更明確的方法來編寫 where
子句。因此,這個技巧可能並不適合所有人。但我認為只要你對你的方法保持一致,這兩種方法都很好。
希望本文能向你展示一些使用 Laravel 模型的新技巧。你現在應該能夠發現並防止 N 1 問題,防止訪問缺失的屬性,防止靜默丟棄屬性,並將主鍵類型更改為 UUID 或 ULID。你還應該知道如何更改用於路由模型綁定的字段,指定返回的集合類型,比較模型以及在構建查詢時使用 whereBelongsTo
。
以上是Laravel模型提示的詳細內容。更多資訊請關注PHP中文網其他相關文章!