首頁  >  文章  >  php框架  >  Laravel 的 N+1 問題解決方法

Laravel 的 N+1 問題解決方法

Guanhui
Guanhui轉載
2020-05-15 10:07:223810瀏覽

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 Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreatePostsTable extends Migration
{
    /**
     * 执行迁移
     *
     * @return void
     */
    public function up()
    {
        Schema::create(&#39;posts&#39;, function (Blueprint $table) {
            $table->increments(&#39;id&#39;);
            $table->unsignedInteger(&#39;author_id&#39;);
            $table->string(&#39;title&#39;);
            $table->text(&#39;body&#39;);
            $table->timestamps();
        });
    }
    /**
     * 回滚迁移
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists(&#39;posts&#39;);
    }
}
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateAuthorsTable extends Migration
{
    /**
     * 执行迁移
     *
     * @return void
     */
    public function up()
    {
        Schema::create(&#39;authors&#39;, function (Blueprint $table) {
            $table->increments(&#39;id&#39;);
            $table->string(&#39;name&#39;);
            $table->text(&#39;bio&#39;);
            $table->timestamps();
        });
    }
    /**
     * 回滚迁移
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists(&#39;authors&#39;);
    }
}
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateProfilesTable extends Migration
{
    /**
     * 执行迁移
     *
     * @return void
     */
    public function up()
    {
        Schema::create(&#39;profiles&#39;, function (Blueprint $table) {
            $table->increments(&#39;id&#39;);
            $table->unsignedInteger(&#39;author_id&#39;);
            $table->date(&#39;birthday&#39;);
            $table->string(&#39;city&#39;);
            $table->string(&#39;state&#39;);
            $table->string(&#39;website&#39;);
            $table->timestamps();
        });
    }
    /**
     * 回滚迁移
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists(&#39;profiles&#39;);
    }
}

#模型

你需要定義模型關聯並透過預先載入來進行更多的實驗。當你運行 php artisan make:model 指令的時候,它將為你建立模型檔。

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

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

接下來, app\Author.php 模型有兩個關聯關係:

<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Author extends Model
{
    public function profile()
    {
        return $this->hasOne(Profile::class);
    }
    public function posts()
    {
        return $this->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 \Illuminate\Database\Eloquent\Factory $factory */
$factory->define(App\Post::class, function (Faker\Generator $faker) {
    return [
        &#39;title&#39; => $faker->sentence,
        &#39;author_id&#39; => function () {
            return factory(App\Author::class)->create()->id;
        },
        &#39;body&#39; => $faker->paragraphs(rand(3,10), true),
    ];
});
/** @var \Illuminate\Database\Eloquent\Factory $factory */
$factory->define(App\Author::class, function (Faker\Generator $faker) {
    return [
        &#39;name&#39; => $faker->name,
        &#39;bio&#39; => $faker->paragraph,
    ];
});
$factory->define(App\Profile::class, function (Faker\Generator $faker) {
    return [
        &#39;birthday&#39; => $faker->dateTimeBetween(&#39;-100 years&#39;, &#39;-18 years&#39;),
        &#39;author_id&#39; => function () {
            return factory(App\Author::class)->create()->id;
        },
        &#39;city&#39; => $faker->city,
        &#39;state&#39; => $faker->state,
        &#39;website&#39; => $faker->domainName,
    ];
});

這些工廠模型可以很容易的填充一些我們可以用來查詢的資料;我們也可以使用它們來建立並產生關聯模型所需的資料。

開啟 database/seeds/DatabaseSeeder.php 檔案將下列內容新增至 DatabaseSeeder::run() 方法:

public function run()
{
    $authors = factory(App\Author::class, 5)->create();
    $authors->each(function ($author) {
        $author
            ->profile()
            ->save(factory(App\Profile::class)->make());
        $author
            ->posts()
            ->saveMany(
                factory(App\Post::class, rand(20,30))->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 App\Providers;
use DB;
use Log;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        DB::listen(function($query) {
            Log::info(
                $query->sql,
                $query->bindings,
                $query->time
            );
        });
    }
    // ...
}

我喜欢把这个监听器封装在配置检查的时候,以便可以控制记录查询日志的开关。你也可以从 Laravel Debugbar 获取到更多相关的信息。

首先,尝试一下在不使用预加载模型的时候,会发生什么情况。清除你的 storage/log/laravel.log 文件当中的内容然后运行 "tinker" 命令:

php artisan tinker
>>> $posts = App\Post::all();
>>> $posts->map(function ($post) {
...     return $post->author;
... });
>>> ...

这个时候检查你的 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
>>> $posts = App\Post::with(&#39;author&#39;)->get();
>>> $posts->map(function ($post) {
...     return $post->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 = App\Post::with([&#39;author&#39;, &#39;comments&#39;])->get();

在 Eloquent 中嵌套预加载

嵌套预加载来做相同的工作。在我们的例子中,每个作者的 model 都有一个关联的个人简介。因此,我们将针对每个个人简介来进行查询。

清空 laravel.log 文件,来做一次尝试:

php artisan tinker
>>> $posts = App\Post::with(&#39;author&#39;)->get();
>>> $posts->map(function ($post) {
...     return $post->author->profile;
... });
...

你现在可以看到七个查询语句,前两个是预加载的结果。然后,我们每次获取一个新的个人简介时,就需要来查询所有作者的个人简介。

通过预加载,我们可以避免嵌套在模型关联中的额外的查询。最后一次清空 laravel.log 文件并运行一下命令:

>>> $posts = App\Post::with(&#39;author.profile&#39;)->get();
>>> $posts->map(function ($post) {
...     return $post->author->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
>>> $posts = App\Post::all();
...
>>> $posts->load(&#39;author.profile&#39;);
>>> $posts->first()->author->profile;
...

你应该只能看到三条查询,并且是在调用 $posts->load() 方法后。

总结

希望你能了解到更多关于预加载模型的相关知识,并且了解它是如何在更加深入底层的工作方式。 预加载文档 是非常全面的,我希望额外的一些代码实现可以帮助您更好的优化关联查询。

推荐教程:《Laravel教程》《PHP教程

以上是Laravel 的 N+1 問題解決方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:learnku.com。如有侵權,請聯絡admin@php.cn刪除