Laravel 的 N+1 問題解決方法

Laravel 的 N+1 問題解決方法

對象關系映射(ORM)使得處理數據驚人地簡單。由于以面向對象的方式定義數據之間關系使得查詢關聯模型數據變得容易,開發者不太需要關注數據底層調用。

ORM 的標準數據優化是渴望式加載相關數據。我們將建立一些示例關系,然后逐步了解查詢隨著渴望式加載和非渴望式加載變化。我喜歡直接使用代碼來試驗一些東西,并通過一些示例來說明渴望式加載是如何工作的,這將進一步幫助你理解如何優化查詢。

介紹

在基本級別,ORM 是 “懶惰” 加載相關的模型數據。但是,ORM 應該如何知道你的意圖?在查詢模型后,您可能永遠不會真正使用相關模型的數據。不優化查詢被稱為 “N + 1” 問題。當您使用對象來表示查詢時,您可能在不知情的情況下進行查詢。

想象一下,您收到了 100 個來自數據庫的對象,并且每條記錄都有 1 個關聯的模型(即 belongsTo)。使用 ORM 默認會產生 101 條查詢;對原始 100 條記錄 進行一次查詢,如果訪問了模型對象上的相關數據,則對每條記錄進行附加查詢。在偽代碼中,假設您要列出所有已發布帖子的發布作者。從一組帖子(每個帖子有一位作者),您可以得到一個作者姓名列表,如下所示:

$posts?=?Post::published()->get();?//?一次查詢 $authors?=?array_map(function($post)?{ ????//?生成對作者模型的查詢 ????return?$post->author->name; },?$posts);

我們并沒有告訴模型我們需要所有作者,因此每次從各個 Post 模型實例中獲取作者姓名時都會發生單獨的查詢 。

預加載

正如我所提到的,ORM 是 “懶惰” 加載關聯。如果您打算使用關聯的模型數據,則可以使用預加載將 101 次查詢縮減為 2 次查詢。您只需要告訴模型你渴望它加載什么。

以下是使用預加載的 Rails Active Record guide 中的示例.。正如您所看到的,這個概念與 laravel’s eager loading 概念非常相似。

#?Rails posts?=?Post.includes(:author).limit(100) #?Laravel $posts?=?Post::with('author')->limit(100)->get();

通過從更廣闊的視角探索,我發現我獲得了更好的理解。Active Record 文檔涵蓋了一些可以進一步幫助該想法產生共鳴的示例。

Laravel 的 Eloquent ORM

Laravel 的 ORM,叫作 Eloquent, 可以很輕松的預加載模型,甚至預加載嵌套關聯模型。讓我們以 Post 模型為例,學習如何在 Laravel 項目中使用預先加載。

我們將使用這個項目構建,然后更深入地瀏覽一些預加載示例以進行總結。

構建

讓我們構建一些 數據庫遷移, 模型, 和? 數據庫種子 來體驗預加載。如果你想跟著操作,我假設你有權訪問數據庫并且可以完成了基本的 Laravel 安裝。

使用 Laravel 安裝器,新建項目:

laravel?new?blog-example

根據你的數據庫和選擇編輯 .env 文件。

接下來,我們將創建三個模型,以便您可以嘗試預加載嵌套關系。這個例子很簡單,所以我們可以專注于預加載,而且我省略了你可能會使用的東西,如索引和外鍵約束。

php?artisan?make:model?-m?Post php?artisan?make:model?-m?Author php?artisan?make:model?-m?Profile

該 -m 標志創建一個遷移,以與將用于創建表模式的模型一起使用。

數據模型將具有以下關聯:

Post -> belongsTo -> Author

Author -> hasMany -> Post

Author -> hasOne -> Profile

遷移

讓我們為每個數據表創建一個簡表結構。我只添加了 up() 方法,因為 Laravel 將會為新的數據表自動添加 down() 方法。這些遷移文件放在了 database/migrations/ 目錄中:

<?php use IlluminateSupportFacadesSchema; use IlluminateDatabaseSchemaBlueprint; use IlluminateDatabaseMigrationsMigration; class CreatePostsTable extends Migration {     /**      * 執行遷移      *      * @return void      */     public function up()     {         Schema::create(&#39;posts&#39;, function (Blueprint $table) {             $table->increments('id'); ????????????$table-&gt;unsignedInteger('author_id'); ????????????$table-&gt;string('title'); ????????????$table-&gt;text('body'); ????????????$table-&gt;timestamps(); ????????}); ????} ????/** ?????*?回滾遷移 ?????* ?????*?@return?void ?????*/ ????public?function?down() ????{ ????????Schema::dropIfExists('posts'); ????} }
<?php use IlluminateSupportFacadesSchema; use IlluminateDatabaseSchemaBlueprint; use IlluminateDatabaseMigrationsMigration; class CreateAuthorsTable extends Migration {     /**      * 執行遷移      *      * @return void      */     public function up()     {         Schema::create(&#39;authors&#39;, function (Blueprint $table) {             $table->increments('id'); ????????????$table-&gt;string('name'); ????????????$table-&gt;text('bio'); ????????????$table-&gt;timestamps(); ????????}); ????} ????/** ?????*?回滾遷移 ?????* ?????*?@return?void ?????*/ ????public?function?down() ????{ ????????Schema::dropIfExists('authors'); ????} }
<?php use IlluminateSupportFacadesSchema; use IlluminateDatabaseSchemaBlueprint; use IlluminateDatabaseMigrationsMigration; class CreateProfilesTable extends Migration {     /**      * 執行遷移      *      * @return void      */     public function up()     {         Schema::create(&#39;profiles&#39;, function (Blueprint $table) {             $table->increments('id'); ????????????$table-&gt;unsignedInteger('author_id'); ????????????$table-&gt;date('birthday'); ????????????$table-&gt;string('city'); ????????????$table-&gt;string('state'); ????????????$table-&gt;string('website'); ????????????$table-&gt;timestamps(); ????????}); ????} ????/** ?????*?回滾遷移 ?????* ?????*?@return?void ?????*/ ????public?function?down() ????{ ????????Schema::dropIfExists('profiles'); ????} }

模型

你需要定義模型關聯并通過預先加載來進行更多的實驗。當你運行 php artisan make:model 命令的時候,它將為你創建模型文件。

第一個模型為 app/Post.php :

<?php namespace App; use IlluminateDatabaseEloquentModel; class Post extends Model {     public function author()     {         return $this->belongsTo(Author::class); ????} }

接下來, appAuthor.php 模型有兩個關聯關系:

<?php namespace App; use IlluminateDatabaseEloquentModel; class Author extends Model {     public function profile()     {         return $this->hasOne(Profile::class); ????} ????public?function?posts() ????{ ????????return?$this-&gt;hasMany(Post::class); ????} }

通過模型和遷移,你可以運行遷移并繼續嘗試使用一些種子模型數據進行預加載。

php?artisan?migrate Migration?table?created?successfully. Migrating:?2014_10_12_000000_create_users_table Migrated:??2014_10_12_000000_create_users_table Migrating:?2014_10_12_100000_create_password_resets_table Migrated:??2014_10_12_100000_create_password_resets_table Migrating:?2017_08_04_042509_create_posts_table Migrated:??2017_08_04_042509_create_posts_table Migrating:?2017_08_04_042516_create_authors_table Migrated:??2017_08_04_042516_create_authors_table Migrating:?2017_08_04_044554_create_profiles_table Migrated:??2017_08_04_044554_create_profiles_table

如果你查看下數據庫,你就會看到所有已經創建好的數據表!

工廠模型

為了讓我們可以運行查詢語句,我們需要創建一些假數據來提供查詢,讓我們添加一些工廠模型,使用這些模型來為數據庫提供測試數據。

打開 database/factories/ModelFactory.php 文件并且將如下三個工廠模型添加到現有的 User 工廠模型文件中:

/**?@var?IlluminateDatabaseEloquentFactory?$factory?*/ $factory-&gt;define(AppPost::class,?function?(FakerGenerator?$faker)?{ ????return?[ ????????'title'?=&gt;?$faker-&gt;sentence, ????????'author_id'?=&gt;?function?()?{ ????????????return?factory(AppAuthor::class)-&gt;create()-&gt;id; ????????}, ????????'body'?=&gt;?$faker-&gt;paragraphs(rand(3,10),?true), ????]; }); /**?@var?IlluminateDatabaseEloquentFactory?$factory?*/ $factory-&gt;define(AppAuthor::class,?function?(FakerGenerator?$faker)?{ ????return?[ ????????'name'?=&gt;?$faker-&gt;name, ????????'bio'?=&gt;?$faker-&gt;paragraph, ????]; }); $factory-&gt;define(AppProfile::class,?function?(FakerGenerator?$faker)?{ ????return?[ ????????'birthday'?=&gt;?$faker-&gt;dateTimeBetween('-100?years',?'-18?years'), ????????'author_id'?=&gt;?function?()?{ ????????????return?factory(AppAuthor::class)-&gt;create()-&gt;id; ????????}, ????????'city'?=&gt;?$faker-&gt;city, ????????'state'?=&gt;?$faker-&gt;state, ????????'website'?=&gt;?$faker-&gt;domainName, ????]; });

這些工廠模型可以很容易的填充一些我們可以用來查詢的數據;我們也可以使用它們來創建并生成關聯模型所需的數據。

打開 database/seeds/DatabaseSeeder.php 文件將以下內容添加到 DatabaseSeeder::run() 方法中:

public?function?run() { ????$authors?=?factory(AppAuthor::class,?5)-&gt;create(); ????$authors-&gt;each(function?($author)?{ ????????$author ????????????-&gt;profile() ????????????-&gt;save(factory(AppProfile::class)-&gt;make()); ????????$author ????????????-&gt;posts() ????????????-&gt;saveMany( ????????????????factory(AppPost::class,?rand(20,30))-&gt;make() ????????????); ????}); }

你創建了五個 author 并遍歷循環每一個 author ,創建和保存了每個 author 相關聯的 profile 和 posts (每個 author 的 posts 的數量在 20 和 30 個之間)。

我們已經完成了遷移、模型、工廠模型和數據庫填充的創建工作,將它們組合起來可以以重復的方式重新運行遷移和數據庫填充:

php?artisan?migrate:refresh php?artisan?db:seed

你現在應該有一些已經填充的數據,可以在下一章節使用它們。注意在 Laravel 5.5 版本中包含一個 migrate:fresh 命令,它會刪除表,而不是回滾遷移并重新應用它們。

嘗試使用預加載

現在我們的前期工作終于已經完成了。 我個人認為最好的可視化方式就是將查詢結果記錄到 storage/logs/laravel.log 文件當中查看。

要把查詢結果記錄到日志中,有兩種方式。第一種,可以開啟 mysql 的日志文件,第二種,則是使用 Eloquent 的數據庫調用來實現。通過 Eloquent 來實現記錄查詢語句的話,可以將下面的代碼添加到 app/Providers/AppServiceProvider.php boot () 方法當中:

namespace?AppProviders; use?DB; use?Log; use?IlluminateSupportServiceProvider; class?AppServiceProvider?extends?ServiceProvider { ????/** ?????*?Bootstrap?any?application?services. ?????* ?????*?@return?void ?????*/ ????public?function?boot() ????{ ????????DB::listen(function($query)?{ ????????????Log::info( ????????????????$query-&gt;sql, ????????????????$query-&gt;bindings, ????????????????$query-&gt;time ????????????); ????????}); ????} ????//?... }

我喜歡把這個監聽器封裝在配置檢查的時候,以便可以控制記錄查詢日志的開關。你也可以從 Laravel Debugbar 獲取到更多相關的信息。

首先,嘗試一下在不使用預加載模型的時候,會發生什么情況。清除你的 storage/log/laravel.log 文件當中的內容然后運行 “tinker” 命令:

php?artisan?tinker &gt;&gt;&gt;?$posts?=?AppPost::all(); &gt;&gt;&gt;?$posts-&gt;map(function?($post)?{ ...?????return?$post-&gt;author; ...?}); &gt;&gt;&gt;?...

這個時候檢查你的 laravel.log 文件,你會發現一查詢作者的查詢語句:

[2017-08-04?06:21:58]?local.INFO:?select?*?from?`posts` [2017-08-04?06:22:06]?local.INFO:?select?*?from?`authors`?where?`authors`.`id`?=???limit?1?[1] [2017-08-04?06:22:06]?local.INFO:?select?*?from?`authors`?where?`authors`.`id`?=???limit?1?[1] [2017-08-04?06:22:06]?local.INFO:?select?*?from?`authors`?where?`authors`.`id`?=???limit?1?[1] ....

然后,再次清空 laravel.log 文件,, 這次使用 with() 方法來用預加載查詢作者信息:

php?artisan?tinker &gt;&gt;&gt;?$posts?=?AppPost::with('author')-&gt;get(); &gt;&gt;&gt;?$posts-&gt;map(function?($post)?{ ...?????return?$post-&gt;author; ...?}); ...

這次你應該看到了,只有兩條查詢語句。一條是對所有帖子進行查詢,以及對帖子所關聯的作者進行查詢:

[2017-08-04?07:18:02]?local.INFO:?select?*?from?`posts` [2017-08-04?07:18:02]?local.INFO:?select?*?from?`authors`?where?`authors`.`id`?in?(?,??,??,??,??)?[1,2,3,4,5]

如果你有多個關聯的模型,你可以使用一個數組進行預加載的實現:

$posts?=?AppPost::with(['author',?'comments'])-&gt;get();

在 Eloquent 中嵌套預加載

嵌套預加載來做相同的工作。在我們的例子中,每個作者的 model 都有一個關聯的個人簡介。因此,我們將針對每個個人簡介來進行查詢。

清空 laravel.log 文件,來做一次嘗試:

php?artisan?tinker &gt;&gt;&gt;?$posts?=?AppPost::with('author')-&gt;get(); &gt;&gt;&gt;?$posts-&gt;map(function?($post)?{ ...?????return?$post-&gt;author-&gt;profile; ...?}); ...

你現在可以看到七個查詢語句,前兩個是預加載的結果。然后,我們每次獲取一個新的個人簡介時,就需要來查詢所有作者的個人簡介。

通過預加載,我們可以避免嵌套在模型關聯中的額外的查詢。最后一次清空 laravel.log 文件并運行一下命令:

&gt;&gt;&gt;?$posts?=?AppPost::with('author.profile')-&gt;get(); &gt;&gt;&gt;?$posts-&gt;map(function?($post)?{ ...?????return?$post-&gt;author-&gt;profile; ...?});

現在,總共有三個查詢語句:

[2017-08-04?07:27:27]?local.INFO:?select?*?from?`posts` [2017-08-04?07:27:27]?local.INFO:?select?*?from?`authors`?where?`authors`.`id`?in?(?,??,??,??,??)?[1,2,3,4,5] [2017-08-04?07:27:27]?local.INFO:?select?*?from?`profiles`?where?`profiles`.`author_id`?in?(?,??,??,??,??)?[1,2,3,4,5]

懶人預加載

你可能只需要收集關聯模型的一些基礎的條件。在這種情況下,可以懶惰地調用關聯數據的一些其他查詢:

php?artisan?tinker &gt;&gt;&gt;?$posts?=?AppPost::all(); ... &gt;&gt;&gt;?$posts-&gt;load('author.profile'); &gt;&gt;&gt;?$posts-&gt;first()-&gt;author-&gt;profile; ...

你應該只能看到三條查詢,并且是在調用 $posts->load() 方法后。

總結

希望你能了解到更多關于預加載模型的相關知識,并且了解它是如何在更加深入底層的工作方式。 預加載文檔 是非常全面的,我希望額外的一些代碼實現可以幫助您更好的優化關聯查詢。

推薦教程:《Laravel教程》《Laravel教程

? 版權聲明
THE END
喜歡就支持一下吧
點贊6 分享