首頁 >後端開發 >php教程 >學會在Laravel中掌握查詢範圍

學會在Laravel中掌握查詢範圍

Emily Anne Brown
Emily Anne Brown原創
2025-03-06 02:28:09513瀏覽
<p><img src="/static/imghwm/default1.png" data-src="https://img.php.cn/upload/article/000/000/000/174119929536701.jpg" class="lazy" alt="Learn to master Query Scopes in Laravel"></p> <p>在構建Laravel應用程序時,您可能需要編寫具有約束條件的查詢,這些約束條件在整個應用程序中的多個地方使用。也許您正在構建一個多租戶應用程序,並且您必須不斷向查詢中添加<code>where</code>約束以按用戶的團隊進行篩選。或者,也許您正在構建一個博客,並且您必須不斷向查詢中添加<code>where</code>約束以篩選博客文章是否已發布。 </p> <p>在Laravel中,我們可以使用查詢範圍來幫助我們將這些約束條件整潔地保存在一個地方並重複使用。 </p> <p>在本文中,我們將研究局部查詢範圍和全局查詢範圍。我們將學習兩者之間的區別,如何創建您自己的查詢範圍,以及如何編寫它們的測試。 </p> <p>在閱讀完本文後,您應該能夠自信地在Laravel應用程序中使用查詢範圍。 </p> <h1>什麼是查詢範圍? </h1> <hr> <p>查詢範圍允許您以可重用的方式在Eloquent查詢中定義約束條件。它們通常定義為Laravel模型上的方法,或者作為實現<code>IlluminateDatabaseEloquentScope</code>接口的類。 </p> <p>它們不僅非常適合在一個地方定義可重用的邏輯,而且還可以通過將復雜的查詢約束隱藏在簡單的函數調用之後來使您的代碼更具可讀性。 </p> <p>查詢範圍分為兩種類型:</p> <ul> <li>局部查詢範圍 - 您必須手動將這些範圍應用於您的查詢。 </li> <li>全局查詢範圍 - 默認情況下,這些範圍會應用於模型上的所有查詢,前提是已註冊該查詢。 </li> </ul> <p>如果您曾經使用過Laravel內置的“軟刪除”功能,您可能已經在不知不覺中使用了查詢範圍。 Laravel使用局部查詢範圍為您提供模型上的<code>withTrashed</code>和<code>onlyTrashed</code>等方法。它還使用全局查詢範圍自動向模型上的所有查詢添加<code>whereNull('deleted_at')</code>約束,以便默認情況下查詢中不會返回軟刪除的記錄。 </p> <p>讓我們來看看如何在Laravel應用程序中創建和使用局部查詢範圍和全局查詢範圍。 </p> <h1>局部查詢範圍</h1> <hr> <p>局部查詢範圍定義為Eloquent模型上的方法,允許您定義可以手動應用於模型查詢的約束條件。 </p> <p>假設我們正在構建一個具有管理面板的博客應用程序。在管理面板中,我們有兩個頁面:一個用於列出已發布的博客文章,另一個用於列出未發布的博客文章。 </p> <p>我們假設博客文章是使用<code>AppModelsArticle</code>模型訪問的,並且數據庫表具有一個可為空的<code>published_at</code>列,用於存儲博客文章的發佈時間。如果<code>published_at</code>列在過去,則該博客文章被認為已發布。如果<code>published_at</code>列在未來或為<code>null</code>,則該博客文章被認為未發布。 </p> <p>要獲取已發布的博客文章,我們可以編寫如下查詢:</p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; $publishedPosts = Article::query() ->where('published_at', '<', now()) ->get();</code></pre> <p>要獲取未發布的博客文章,我們可以編寫如下查詢:</p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; use Illuminate\Contracts\Database\Eloquent\Builder; $unpublishedPosts = Article::query() ->where(function (Builder $query): void { $query->whereNull('published_at') ->orWhere('published_at', '>', now()); }) ->get();</code></pre> <p>上面的查詢並不特別複雜。但是,假設我們在整個應用程序中的多個地方使用它們。隨著出現次數的增加,我們犯錯或忘記在一個地方更新查詢的可能性越來越大。例如,開發人員可能會意外地使用<code>>=</code>而不是<code><</code>來查詢已發布的博客文章。或者,確定博客文章是否已發布的邏輯可能會更改,我們需要更新所有查詢。 </p> <p>這就是查詢範圍非常有用的地方。因此,讓我們通過在<code>AppModelsArticle</code>模型上創建局部查詢範圍來整理我們的查詢。 </p> <p>局部查詢範圍是通過創建一個以<code>scope</code>開頭並以範圍的預期名稱結尾的方法來定義的。例如,名為<code>scopePublished</code>的方法將在模型上創建一個<code>published</code>範圍。該方法應該接受一個<code>IlluminateContractsDatabaseEloquentBuilder</code>實例並返回一個<code>IlluminateContractsDatabaseEloquentBuilder</code>實例。 </p> <p>我們將這兩個範圍都添加到<code>AppModelsArticle</code>模型中:</p> <pre class="brush:php;toolbar:false"><code>declare(strict_types=1); namespace App\Models; use Illuminate\Contracts\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; final class Article extends Model { public function scopePublished(Builder $query): Builder { return $query->where('published_at', '<', now()); } public function scopeNotPublished(Builder $query): Builder { return $query->where(function (Builder $query): Builder { return $query->whereNull('published_at') ->orWhere('published_at', '>', now()); }); } // ... }</code> </p> <p>正如我們在上面的示例中看到的,我們將<code>where</code>約束從之前的查詢移動到了兩個單獨的方法中:<code>scopePublished</code>和<code>scopeNotPublished</code>。我們現在可以在我們的查詢中像這樣使用這些範圍:</p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; $publishedPosts = Article::query() ->published() ->get(); $unpublishedPosts = Article::query() ->notPublished() ->get();</code></pre> <p>在我個人看來,我發現這些查詢更容易閱讀和理解。這也意味著如果我們將來需要使用相同約束條件編寫任何查詢,我們可以重複使用這些範圍。 </p> <h1>全局查詢範圍</h1> <hr> <p>全局查詢範圍執行與局部查詢範圍類似的功能。但是,它不是在逐個查詢的基礎上手動應用,而是自動應用於模型上的所有查詢。 </p> <p>正如我們前面提到的,Laravel內置的“軟刪除”功能使用了<code>IlluminateDatabaseEloquentSoftDeletingScope</code>全局查詢範圍。此範圍會自動向模型上的所有查詢添加<code>whereNull('deleted_at')</code>約束。如果您有興趣了解其底層工作原理,可以在這裡查看GitHub上的源代碼。 </p> <p>例如,假設您正在構建一個具有管理面板的多租戶博客應用程序。您可能只想允許用戶查看屬於其團隊的文章。因此,您可能會編寫如下查詢:</p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; $publishedPosts = Article::query() ->where('published_at', '<', now()) ->get();</code></pre> <p>此查詢很好,但很容易忘記添加<code>where</code>約束。如果您正在編寫另一個查詢並忘記添加約束,則最終會在您的應用程序中出現一個錯誤,該錯誤將允許用戶與不屬於其團隊的文章進行交互。當然,我們不希望發生這種情況! </p> <p>為了防止這種情況,我們可以創建一個全局範圍,我們可以將其自動應用於我們所有<code>AppModelArticle</code>模型查詢。 </p> <h3>#如何創建全局查詢範圍</h3> <p>讓我們創建一個全局查詢範圍,該範圍按<code>team_id</code>列過濾所有查詢。 </p> <p>請注意,為了本文的目的,我們保持示例簡單。在實際應用程序中,您可能希望使用更強大的方法來處理用戶未經身份驗證或用戶屬於多個團隊等情況。但現在,讓我們保持簡單,以便我們可以專注於全局查詢範圍的概念。 </p> <p>我們將首先在終端中運行以下Artisan命令:</p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; use Illuminate\Contracts\Database\Eloquent\Builder; $unpublishedPosts = Article::query() ->where(function (Builder $query): void { $query->whereNull('published_at') ->orWhere('published_at', '>', now()); }) ->get();</code></pre> <p>這應該已經創建了一個新的<code>app/Models/Scopes/TeamScope.php</code>文件。我們將對此文件進行一些更新,然後查看完成的代碼:</p> <pre class="brush:php;toolbar:false"><code>declare(strict_types=1); namespace App\Models; use Illuminate\Contracts\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; final class Article extends Model { public function scopePublished(Builder $query): Builder { return $query->where('published_at', '<', now()); } public function scopeNotPublished(Builder $query): Builder { return $query->where(function (Builder $query): Builder { return $query->whereNull('published_at') ->orWhere('published_at', '>', now()); }); } // ... }</code></pre> <p>在上面的代碼示例中,我們可以看到我們有一個新的類,它實現了<code>IlluminateDatabaseEloquentScope</code>接口並具有一個名為<code>apply</code>的單個方法。這就是我們定義要應用於模型查詢的約束條件的方法。 </p> <p>我們的全局範圍現在可以使用了。我們可以將其添加到任何我們想要將查詢範圍縮小到用戶團隊的模型中。 </p> <p>讓我們將其應用於<code>AppModelsArticle</code>模型。 </p> <h3>#應用全局查詢範圍</h3> <p>有多種方法可以將全局範圍應用於模型。第一種方法是在模型上使用<code>IlluminateDatabaseEloquentAttributesScopedBy</code>屬性:</p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; $publishedPosts = Article::query() ->published() ->get(); $unpublishedPosts = Article::query() ->notPublished() ->get();</code></pre> <p>另一種方法是在模型的<code>booted</code>方法中使用<code>addGlobalScope</code>方法:</p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; $articles = Article::query() ->where('team_id', Auth::user()->team_id) ->get();</code></pre> <p>這兩種方法都將<code>where('team_id', Auth::user()->team_id)</code>約束應用於<code>AppModelsArticle</code>模型上的所有查詢。 </p> <p>這意味著您現在可以編寫查詢,而無需擔心按<code>team_id</code>列進行過濾:</p> <pre class="brush:php;toolbar:false"><code>php artisan make:scope TeamScope</code></pre> <p>如果我們假設用戶屬於<code>team_id</code>為<code>1</code>的團隊,則將為上面的查詢生成以下SQL:</p> <pre class="brush:php;toolbar:false"><code>declare(strict_types=1); namespace App\Models\Scopes; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Scope; use Illuminate\Support\Facades\Auth; final readonly class TeamScope implements Scope { /** * Apply the scope to a given Eloquent query builder. */ public function apply(Builder $builder, Model $model): void { $builder->where('team_id', Auth::user()->team_id); } }</code></pre> <p>這很酷,對吧? ! </p> <h3>#匿名全局查詢範圍</h3> <p>定義和應用全局查詢範圍的另一種方法是使用匿名全局範圍。 </p> <p>讓我們更新我們的<code>AppModelsArticle</code>模型以使用匿名全局範圍:</p> <pre class="brush:php;toolbar:false"><code>declare(strict_types=1); namespace App\Models; use App\Models\Scopes\TeamScope; use Illuminate\Database\Eloquent\Attributes\ScopedBy; use Illuminate\Database\Eloquent\Model; #[ScopedBy(TeamScope::class)] final class Article extends Model { // ... }</code></pre> <p>在上面的代碼示例中,我們使用了<code>addGlobalScope</code>方法在模型的<code>booted</code>方法中定義匿名全局範圍。 <code>addGlobalScope</code>方法接受兩個參數:</p> <ul> <li>範圍的名稱 - 如果您需要在查詢中忽略它,則可以使用此名稱來引用範圍</li> <li>範圍約束 - 定義要應用於查詢的約束的閉包</li> </ul> <p>與其他方法一樣,這會將<code>where('team_id', Auth::user()->team_id)</code>約束應用於<code>AppModelsArticle</code>模型上的所有查詢。 </p> <p>根據我的經驗,匿名全局範圍不如在單獨的類中定義全局範圍常見。但了解它們可用是很有好處的,以備不時之需。 </p> <h3>#忽略全局查詢範圍</h3> <p>有時您可能希望編寫一個不使用已應用於模型的全局查詢範圍的查詢。例如,您可能正在構建一個需要包含所有記錄的報表或分析查詢,而不管全局查詢範圍如何。 </p> <p>如果是這種情況,您可以使用兩種方法之一來忽略全局範圍。 </p> <p>第一種方法是<code>withoutGlobalScopes</code>。如果未向其傳遞任何參數,此方法允許您忽略模型上的所有全局範圍:</p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; $publishedPosts = Article::query() ->where('published_at', '<', now()) ->get();</code></pre> <p>或者,如果您只想忽略給定的一組全局範圍,您可以將範圍名稱傳遞給<code>withoutGlobalScopes</code>方法:</p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; use Illuminate\Contracts\Database\Eloquent\Builder; $unpublishedPosts = Article::query() ->where(function (Builder $query): void { $query->whereNull('published_at') ->orWhere('published_at', '>', now()); }) ->get();</code></pre> <p>在上面的示例中,我們忽略了<code>AppModelsScopesTeamScope</code>和另一個名為<code>another_scope</code>的虛構匿名全局範圍。 </p> <p>或者,如果您只想忽略單個全局範圍,可以使用<code>withoutGlobalScope</code>方法:</p> <pre class="brush:php;toolbar:false"><code>declare(strict_types=1); namespace App\Models; use Illuminate\Contracts\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; final class Article extends Model { public function scopePublished(Builder $query): Builder { return $query->where('published_at', '<', now()); } public function scopeNotPublished(Builder $query): Builder { return $query->where(function (Builder $query): Builder { return $query->whereNull('published_at') ->orWhere('published_at', '>', now()); }); } // ... }</code></pre> <h3>#全局查詢範圍注意事項</h3> <p>務必記住,全局查詢範圍僅應用於通過模型進行的查詢。如果您使用<code>IlluminateSupportFacadesDB</code>外觀編寫數據庫查詢,則不會應用全局查詢範圍。 </p> <p>例如,假設您編寫了此查詢,您希望它只抓取屬於登錄用戶的團隊的文章:</p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; $publishedPosts = Article::query() ->published() ->get(); $unpublishedPosts = Article::query() ->notPublished() ->get();</code></pre> <p>在上面的查詢中,即使在<code>AppModelsArticle</code>模型上定義了<code>AppModelsScopesTeamScope</code>全局查詢範圍,也不會應用該範圍。因此,您需要確保在數據庫查詢中手動應用約束條件。 </p> <h1>測試局部查詢範圍</h1> <hr> <p>既然我們已經學習瞭如何創建和使用查詢範圍,那麼我們將研究如何為它們編寫測試。 </p> <p>有多種方法可以測試查詢範圍,您選擇的方法可能取決於您的個人喜好或您正在編寫的範圍的內容。例如,您可能希望為範圍編寫更多單元樣式的測試。或者,您可能希望編寫更多集成樣式的測試,這些測試會在諸如控制器之類的上下文中測試範圍。 </p> <p>就我個人而言,我喜歡混合使用兩者,這樣我就可以確信範圍正在添加正確的約束,並且範圍實際上正在查詢中使用。 </p> <p>讓我們從前面的示例<code>published</code>和<code>notPublished</code>範圍開始,並為它們編寫一些測試。我們將需要編寫兩個不同的測試(每個範圍一個):</p> <ul> <li>一個測試檢查<code>published</code>範圍只返回已發布的文章。 </li> <li>一個測試檢查<code>notPublished</code>範圍只返回未發布的文章。 </li> </ul> <p>讓我們看看這些測試,然後討論正在做的事情:</p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; $publishedPosts = Article::query() ->where('published_at', '<', now()) ->get();</code></pre> <p>我們可以在上面的測試文件中看到,我們首先在<code>setUp</code>方法中創建一些數據。我們創建了兩篇已發布的文章、一篇未安排的文章和一篇已安排的文章。 </p> <p>然後是一個測試(<code>only_published_articles_are_returned</code>),它檢查<code>published</code>範圍只返回已發布的文章。還有一個測試(<code>only_not_published_articles_are_returned</code>),它檢查<code>notPublished</code>範圍只返回未發布的文章。 </p> <p>通過這樣做,我們現在可以確信我們的查詢範圍正在按預期應用約束條件。 </p> <h1>在控制器中測試範圍</h1> <hr> <p>正如我們提到的,測試查詢範圍的另一種方法是在控制器中使用的上下文中測試它們。雖然範圍的隔離測試可以幫助斷言範圍正在向查詢添加正確的約束,但它實際上並沒有測試範圍是否按預期在應用程序中使用。例如,您可能忘記向控制器方法中的查詢添加<code>published</code>範圍。 </p> <p>通過編寫斷言在控制器方法中使用範圍時返回正確數據的測試,可以捕獲這些類型的錯誤。 </p> <p>讓我們以具有多租戶博客應用程序的示例為例,並為列出文章的控制器方法編寫一個測試。我們假設我們有一個非常簡單的控制器方法,如下所示:</p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; use Illuminate\Contracts\Database\Eloquent\Builder; $unpublishedPosts = Article::query() ->where(function (Builder $query): void { $query->whereNull('published_at') ->orWhere('published_at', '>', now()); }) ->get();</code></pre> <p>我們假設<code>AppModelsArticle</code>模型已應用我們的<code>AppModelsScopesTeamScope</code>。 </p> <p>我們將要斷言只返回屬於用戶團隊的文章。測試用例可能如下所示:</p> <pre class="brush:php;toolbar:false"><code>declare(strict_types=1); namespace App\Models; use Illuminate\Contracts\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; final class Article extends Model { public function scopePublished(Builder $query): Builder { return $query->where('published_at', '<', now()); } public function scopeNotPublished(Builder $query): Builder { return $query->where(function (Builder $query): Builder { return $query->whereNull('published_at') ->orWhere('published_at', '>', now()); }); } // ... }</code></pre> <p>在上面的測試中,我們正在創建兩個團隊。然後,我們創建一個屬於團隊一的用戶。我們為團隊一創建 3 篇文章,為團隊二創建 2 篇文章。然後,我們充當用戶並向列出文章的控制器方法發出請求。控制器方法應該只返回屬於團隊一的 3 篇文章,因此我們通過比較文章的 ID 來斷言只返回這些文章。 </p> <p>這意味著我們可以確信全局查詢範圍正在控制器方法中按預期使用。 </p> <h1>結論</h1> <hr> <p>在本文中,我們學習了局部查詢範圍和全局查詢範圍。我們學習了它們之間的區別,如何創建和使用它們,以及如何為它們編寫測試。 </p> <p>希望您現在應該能夠自信地在Laravel應用程序中使用查詢範圍。 </p>

以上是學會在Laravel中掌握查詢範圍的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn