搜索
首页php框架LaravelLaravel 的 N+1 问题解决方法
Laravel 的 N+1 问题解决方法May 15, 2020 am 10:07 AM
laravelorm

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。如有侵权,请联系admin@php.cn删除
laravel单点登录方法详解laravel单点登录方法详解Jun 15, 2022 am 11:45 AM

本篇文章给大家带来了关于laravel的相关知识,其中主要介绍了关于单点登录的相关问题,单点登录是指在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统,下面一起来看一下,希望对大家有帮助。

一起来聊聊Laravel的生命周期一起来聊聊Laravel的生命周期Apr 25, 2022 pm 12:04 PM

本篇文章给大家带来了关于laravel的相关知识,其中主要介绍了关于Laravel的生命周期相关问题,Laravel 的生命周期从public\index.php开始,从public\index.php结束,希望对大家有帮助。

laravel中guard是什么laravel中guard是什么Jun 02, 2022 pm 05:54 PM

在laravel中,guard是一个用于用户认证的插件;guard的作用就是处理认证判断每一个请求,从数据库中读取数据和用户输入的对比,调用是否登录过或者允许通过的,并且Guard能非常灵活的构建一套自己的认证体系。

laravel中asset()方法怎么用laravel中asset()方法怎么用Jun 02, 2022 pm 04:55 PM

laravel中asset()方法的用法:1、用于引入静态文件,语法为“src="{{asset(‘需要引入的文件路径’)}}"”;2、用于给当前请求的scheme前端资源生成一个url,语法为“$url = asset('前端资源')”。

laravel中间件基础详解laravel中间件基础详解May 18, 2022 am 11:46 AM

本篇文章给大家带来了关于laravel的相关知识,其中主要介绍了关于中间件的相关问题,包括了什么是中间件、自定义中间件等等,中间件为过滤进入应用的 HTTP 请求提供了一套便利的机制,下面一起来看一下,希望对大家有帮助。

实例详解laravel使用中间件记录用户请求日志实例详解laravel使用中间件记录用户请求日志Apr 26, 2022 am 11:53 AM

本篇文章给大家带来了关于laravel的相关知识,其中主要介绍了关于使用中间件记录用户请求日志的相关问题,包括了创建中间件、注册中间件、记录用户访问等等内容,下面一起来看一下,希望对大家有帮助。

laravel的fill方法怎么用laravel的fill方法怎么用Jun 06, 2022 pm 03:33 PM

在laravel中,fill方法是一个给Eloquent实例赋值属性的方法,该方法可以理解为用于过滤前端传输过来的与模型中对应的多余字段;当调用该方法时,会先去检测当前Model的状态,根据fillable数组的设置,Model会处于不同的状态。

laravel路由文件在哪个目录里laravel路由文件在哪个目录里Apr 28, 2022 pm 01:07 PM

laravel路由文件在“routes”目录里。Laravel中所有的路由文件定义在routes目录下,它里面的内容会自动被框架加载;该目录下默认有四个路由文件用于给不同的入口使用:web.php、api.php、console.php等。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
2 周前By尊渡假赌尊渡假赌尊渡假赌
仓库:如何复兴队友
1 个月前By尊渡假赌尊渡假赌尊渡假赌
Hello Kitty Island冒险:如何获得巨型种子
4 周前By尊渡假赌尊渡假赌尊渡假赌

热工具

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

螳螂BT

螳螂BT

Mantis是一个易于部署的基于Web的缺陷跟踪工具,用于帮助产品缺陷跟踪。它需要PHP、MySQL和一个Web服务器。请查看我们的演示和托管服务。

MinGW - 适用于 Windows 的极简 GNU

MinGW - 适用于 Windows 的极简 GNU

这个项目正在迁移到osdn.net/projects/mingw的过程中,你可以继续在那里关注我们。MinGW:GNU编译器集合(GCC)的本地Windows移植版本,可自由分发的导入库和用于构建本地Windows应用程序的头文件;包括对MSVC运行时的扩展,以支持C99功能。MinGW的所有软件都可以在64位Windows平台上运行。

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)