Heim  >  Artikel  >  PHP-Framework  >  Laravels N+1-Problemlösung

Laravels N+1-Problemlösung

Guanhui
Guanhuinach vorne
2020-05-15 10:07:223737Durchsuche

Laravels N+1-Problemlösung

Objektrelationales Mapping (ORM) macht die Arbeit mit Daten überraschend einfach. Da die objektorientierte Definition von Beziehungen zwischen Daten die Abfrage verwandter Modelldaten erleichtert, müssen Entwickler nicht auf die zugrunde liegenden Aufrufe der Daten achten.

ORMs Standard-Datenoptimierung besteht darin, relevante Daten eifrig zu laden. Wir richten einige Beispielbeziehungen ein und gehen dann Schritt für Schritt durch, wie sich Abfragen beim Eager- und Non-Eager-Laden ändern. Ich experimentiere gerne direkt mit Code und gehe einige Beispiele durch, um zu veranschaulichen, wie Eager Loading funktioniert. Dies wird Ihnen helfen, besser zu verstehen, wie Sie Ihre Abfragen optimieren können.

Einführung

Grundsätzlich ist ORM „faul“ beim Laden relevanter Modelldaten. Aber woher soll der ORM Ihre Absicht wissen? Nach der Abfrage eines Modells werden Sie die Daten des zugehörigen Modells möglicherweise nie wirklich verwenden. Das Nichtoptimieren einer Abfrage wird als „N+1“-Problem bezeichnet. Wenn Sie Objekte zur Darstellung von Abfragen verwenden, fragen Sie möglicherweise ab, ohne es zu wissen.

Stellen Sie sich vor, Sie erhalten 100 Objekte aus der Datenbank und jeder Datensatz verfügt über ein zugehöriges Modell (d. h. gehört zu). Die Verwendung des ORM führt standardmäßig zu einer Abfrage für die ursprünglichen 100 Datensätze und zusätzlichen Abfragen für jeden Datensatz, wenn auf relevante Daten zum Modellobjekt zugegriffen wird. Nehmen wir im Pseudocode an, Sie möchten die veröffentlichenden Autoren aller veröffentlichten Beiträge auflisten. Aus einer Reihe von Beiträgen (jeder Beitrag hat einen Autor) können Sie eine Liste mit Autorennamen wie folgt erhalten:

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

Wir sagen dem Modell nicht, dass wir alle Autoren benötigen, also jedes Mal aus dem einzelnen Beitrag Modellinstanz Beim Abrufen des Namens des Autors erfolgt eine separate Abfrage.

Vorladen

Wie ich bereits erwähnt habe, sind ORMs „faul“, wenn es um das Laden von Assoziationen geht. Wenn Sie verknüpfte Modelldaten verwenden möchten, können Sie Eager Loading verwenden, um 101 Abfragen auf 2 Abfragen zu reduzieren. Sie müssen dem Modell lediglich mitteilen, was es laden soll.

Das Folgende ist ein Beispiel aus dem Rails Active Record-Leitfaden unter Verwendung des Vorladens. Wie Sie sehen können, ist dieses Konzept dem Eager-Loading-Konzept von Laravel sehr ähnlich.

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

Ich finde, dass ich ein besseres Verständnis gewinne, wenn ich es aus einer breiteren Perspektive erkunde. Die Active Record-Dokumentation enthält einige Beispiele, die dazu beitragen können, dass die Idee noch mehr Anklang findet.

Laravels Eloquent ORM

Laravels ORM namens Eloquent kann problemlos Modelle und sogar verschachtelte relationale Modelle vorab laden. Nehmen wir das Post-Modell als Beispiel, um zu lernen, wie man Eager Loading in Laravel-Projekten nutzt.

Wir werden mit diesem Projekt aufbauen und dann zum Abschluss tiefer in einige Vorladebeispiele eintauchen.

Erstellen

Lassen Sie uns einige Datenbankmigrationen, Modelle und Datenbank-Seeds erstellen, um das Vorladen zu erleben. Wenn Sie mitmachen möchten, gehe ich davon aus, dass Sie Zugriff auf die Datenbank haben und eine grundlegende Laravel-Installation abgeschlossen haben.

Erstellen Sie mit dem Laravel-Installationsprogramm ein neues Projekt:

laravel new blog-example

Bearbeiten Sie die .env-Datei entsprechend Ihrer Datenbank und Auswahl.

Als nächstes erstellen wir drei Modelle, damit Sie versuchen können, verschachtelte Beziehungen vorab zu laden. Dieses Beispiel ist einfach, sodass wir uns auf das Eager Loading konzentrieren können. Ich habe Dinge weggelassen, die Sie möglicherweise verwenden, wie Indizes und Fremdschlüsseleinschränkungen.

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

Das Flag -m erstellt eine Migration zur Verwendung mit dem Modell, das zum Erstellen des Tabellenschemas verwendet wird.

Das Datenmodell hat die folgenden Zuordnungen:

Beitrag -> Autor

Autor -> Beitrag

Autor -> hasOne -> Profil

Migration

Lassen Sie uns eine Profilstruktur für jede Datentabelle erstellen. Ich habe nur die up()-Methode hinzugefügt, weil Laravel automatisch die down()-Methode für neue Tabellen hinzufügt. Diese Migrationsdateien werden im Verzeichnis „database/migrations/“ abgelegt:

<?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;);
    }
}

Model

Sie müssen Modellzuordnungen definieren und weitere Experimente mit Eager Loading durchführen. Wenn Sie den Befehl php artisan make:model ausführen, wird die Modelldatei für Sie erstellt.

Das erste Modell ist app/Post.php:

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

Als nächstes hat das appAuthor.php-Modell zwei Beziehungen:

<?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);
    }
}

Durch das Modell und die Migration können Sie Führen Sie die Migration aus und versuchen Sie weiterhin, einige Seed-Modelldaten vorab zu laden.

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

Wenn Sie in die Datenbank schauen, sehen Sie alle Tabellen, die erstellt wurden!

Fabrikmodell

Damit wir die Abfrageanweisung ausführen können, müssen wir einige gefälschte Daten erstellen, um die Abfrage bereitzustellen. Fügen wir einige Fabrikmodelle hinzu und verwenden Sie diese Modelle zu Die Datenbank stellt Testdaten bereit.

Öffnen Sie die Datei „database/factories/ModelFactory.php“ und fügen Sie die folgenden drei Fabrikmodelle zur vorhandenen Benutzer-Fabrikmodelldatei hinzu:

/** @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,
    ];
});

Diese Fabrikmodelle können problemlos einige unserer Daten ausfüllen können abgefragt werden; wir können sie auch verwenden, um die für relationale Modelle erforderlichen Daten zu erstellen und zu generieren.

Öffnen Sie die Datei „database/seeds/DatabaseSeeder.php“ und fügen Sie den folgenden Inhalt zur Methode „DatabaseSeeder::run()“ hinzu:

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教程

Das obige ist der detaillierte Inhalt vonLaravels N+1-Problemlösung. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:learnku.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen