Maison > cadre php > Laravel > Solution au problème N+1 de Laravel

Solution au problème N+1 de Laravel

Guanhui
Libérer: 2020-05-15 10:07:22
avant
3874 Les gens l'ont consulté

Solution au problème N+1 de Laravel

Le mappage objet-relationnel (ORM) rend l'utilisation des données étonnamment facile. Étant donné que la définition des relations entre les données de manière orientée objet facilite l'interrogation des données de modèle associées, les développeurs n'ont pas besoin de prêter attention aux appels sous-jacents des données.

L'optimisation des données standard d'ORM consiste à charger avec impatience les données pertinentes. Nous allons définir quelques exemples de relations, puis expliquer comment les requêtes changent avec un chargement impatient et non impatient. J'aime expérimenter des choses directement en utilisant du code et parcourir quelques exemples pour illustrer le fonctionnement du chargement rapide, ce qui vous aidera davantage à comprendre comment optimiser vos requêtes.

Introduction

À un niveau de base, ORM est « paresseux » dans le chargement des données de modèle pertinentes. Mais comment l’ORM est-il censé connaître votre intention ? Après avoir interrogé un modèle, vous ne pouvez jamais utiliser les données du modèle associé. La non-optimisation d’une requête est connue sous le nom de problème « N+1 ». Lorsque vous utilisez des objets pour représenter des requêtes, vous effectuez peut-être des requêtes sans le savoir.

Imaginez que vous recevez 100 objets de la base de données et que chaque enregistrement a 1 modèle associé (c'est-à-dire appartient à). L'utilisation de l'ORM entraînera 101 requêtes par défaut ; une requête pour les 100 enregistrements d'origine et des requêtes supplémentaires pour chaque enregistrement si les données pertinentes sur l'objet modèle sont accessibles. En pseudocode, disons que vous souhaitez lister les auteurs de tous les articles publiés. À partir d'un ensemble de publications (chaque publication a un auteur), vous pouvez obtenir une liste de noms d'auteurs comme celle-ci :

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

Nous ne disons pas au modèle que nous avons besoin de tous les auteurs, donc à chaque fois à partir de la publication individuelle instance de modèle Une requête distincte se produit lors de la récupération du nom de l'auteur.

Préchargement

Comme je l'ai mentionné, les ORM sont "paresseux" quant au chargement des associations. Si vous envisagez d'utiliser les données de modèle associées, vous pouvez utiliser le chargement rapide pour réduire 101 requêtes à 2 requêtes. Il vous suffit d'indiquer au modèle ce que vous souhaitez qu'il charge.

Ce qui suit est un exemple tiré du guide Rails Active Record utilisant le préchargement. Comme vous pouvez le constater, ce concept est très similaire au concept de chargement rapide de Laravel.

# Rails
posts = Post.includes(:author).limit(100)
# Laravel
$posts = Post::with('author')->limit(100)->get();
Copier après la connexion

Je trouve que j'acquiers une meilleure compréhension en explorant dans une perspective plus large. La documentation d'Active Record couvre quelques exemples qui peuvent aider davantage l'idée à trouver un écho.

L'ORM Eloquent de Laravel

L'ORM de Laravel, appelé Eloquent, peut facilement précharger des modèles, et même précharger des modèles relationnels imbriqués. Prenons le modèle Post comme exemple pour apprendre à utiliser le chargement rapide dans les projets Laravel.

Nous allons construire avec ce projet, puis approfondir quelques exemples de préchargement pour conclure.

Construire

Créons des migrations de bases de données, des modèles et des graines de base de données pour expérimenter le préchargement. Si vous souhaitez suivre, je suppose que vous avez accès à la base de données et que vous avez effectué une installation de base de Laravel.

À l'aide de l'installateur Laravel, créez un nouveau projet :

laravel new blog-example
Copier après la connexion

Modifiez le fichier .env en fonction de votre base de données et de vos sélections.

Ensuite, nous allons créer trois modèles afin que vous puissiez essayer de précharger des relations imbriquées. Cet exemple est simple afin que nous puissions nous concentrer sur un chargement rapide, et j'ai omis des éléments que vous pourriez utiliser comme les index et les contraintes de clé étrangère.

php artisan make:model -m Post
php artisan make:model -m Author
php artisan make:model -m Profile
Copier après la connexion

L'indicateur -m crée une migration à utiliser avec le modèle qui sera utilisé pour créer le schéma de table.

Le modèle de données aura les associations suivantes :

Post -> >Auteur -> hasOne -> Profil

Migration

Créons une structure de profil pour chaque table de données. J'ai seulement ajouté la méthode up() car Laravel ajoutera automatiquement la méthode down() pour les nouvelles tables. Ces fichiers de migration sont placés dans le répertoire 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;);
    }
}
Copier après la connexion
<?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;);
    }
}
Copier après la connexion
<?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;);
    }
}
Copier après la connexion

Modèle

Vous devez définir des associations de modèles et effectuer davantage d'expériences avec un chargement rapide. Lorsque vous exécutez la commande php artisan make:model, elle créera le fichier modèle pour vous. Le premier modèle est app/Post.php :

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

Ensuite, le modèle appAuthor.php a deux relations :

<?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);
    }
}
Copier après la connexion

Grâce au modèle et à la migration, vous pouvez exécutez la migration et continuez à essayer de précharger avec certaines données du modèle de départ.

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
Copier après la connexion

Si vous regardez dans la base de données, vous verrez toutes les tables qui ont été créées !

Modèle d'usine

Pour que nous puissions exécuter l'instruction de requête, nous devons créer de fausses données pour fournir la requête. Ajoutons quelques modèles d'usine et utilisons-les. modèles à La base de données fournit des données de test. Ouvrez le fichier database/factories/ModelFactory.php et ajoutez les trois modèles d'usine suivants au fichier de modèle d'usine utilisateur existant :

/** @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,
    ];
});
Copier après la connexion

Ces modèles d'usine peuvent facilement remplir certaines de nos données qui peuvent être interrogés ; nous pouvons également les utiliser pour créer et générer les données nécessaires aux modèles relationnels.

Ouvrez le fichier database/seeds/DatabaseSeeder.php et ajoutez le contenu suivant à la méthode 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()
            );
    });
}
Copier après la connexion

你创建了五个 author 并遍历循环每一个 author ,创建和保存了每个 author 相关联的 profile 和 posts (每个 author 的 posts 的数量在 20 和 30 个之间)。

我们已经完成了迁移、模型、工厂模型和数据库填充的创建工作,将它们组合起来可以以重复的方式重新运行迁移和数据库填充:

php artisan migrate:refresh
php artisan db:seed
Copier après la connexion

你现在应该有一些已经填充的数据,可以在下一章节使用它们。注意在 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
            );
        });
    }
    // ...
}
Copier après la connexion

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

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

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

这个时候检查你的 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]
....
Copier après la connexion

然后,再次清空 laravel.log 文件,, 这次使用 with() 方法来用预加载查询作者信息:

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

这次你应该看到了,只有两条查询语句。一条是对所有帖子进行查询,以及对帖子所关联的作者进行查询:

[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]
Copier après la connexion

如果你有多个关联的模型,你可以使用一个数组进行预加载的实现:

$posts = App\Post::with([&#39;author&#39;, &#39;comments&#39;])->get();
Copier après la connexion

在 Eloquent 中嵌套预加载

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

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

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

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

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

>>> $posts = App\Post::with(&#39;author.profile&#39;)->get();
>>> $posts->map(function ($post) {
...     return $post->author->profile;
... });
Copier après la connexion

现在,总共有三个查询语句:

[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]
Copier après la connexion

懒人预加载

你可能只需要收集关联模型的一些基础的条件。在这种情况下,可以懒惰地调用关联数据的一些其他查询:

php artisan tinker
>>> $posts = App\Post::all();
...
>>> $posts->load(&#39;author.profile&#39;);
>>> $posts->first()->author->profile;
...
Copier après la connexion

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

总结

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

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:learnku.com
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal