Laravel의 N+1 문제 솔루션

Guanhui
풀어 주다: 2020-05-15 10:07:22
앞으로
3763명이 탐색했습니다.

Laravel의 N+1 문제 솔루션

ORM(객체 관계형 매핑)을 사용하면 데이터 작업이 놀라울 정도로 쉬워집니다. 객체 지향 방식으로 데이터 간의 관계를 정의하면 관련 모델 데이터를 쉽게 쿼리할 수 있으므로 개발자는 데이터의 기본 호출에 주의를 기울일 필요가 없습니다.

ORM의 표준 데이터 최적화는 관련 데이터를 열심히 로드하는 것입니다. 몇 가지 예제 관계를 설정한 다음 즉시 로딩 및 비즉시 로딩에 따라 쿼리가 어떻게 변경되는지 단계별로 살펴보겠습니다. 나는 코드를 사용하여 직접 실험하고 몇 가지 예를 통해 Eager Loading의 작동 방식을 설명하는 것을 좋아합니다. 이는 쿼리를 최적화하는 방법을 이해하는 데 더 도움이 될 것입니다.

소개

기본적으로 ORM은 관련 모델 데이터를 로드하는 데 "게으르다". 하지만 ORM은 당신의 의도를 어떻게 알 수 있을까요? 모델을 쿼리한 후에는 관련 모델의 데이터를 실제로 사용할 수 없습니다. 쿼리를 최적화하지 않는 것을 "N+1" 문제라고 합니다. 쿼리를 표현하기 위해 객체를 사용할 때, 자신도 모르게 쿼리를 하고 있을 수 있습니다.

데이터베이스에서 100개의 개체를 받고 각 레코드에 1개의 관련 모델(예: 소속)이 있다고 상상해 보세요. ORM을 사용하면 기본적으로 101개의 쿼리가 발생합니다. 원래 100개의 레코드에 대해 하나의 쿼리가 발생하고 모델 개체의 관련 데이터에 액세스하는 경우 각 레코드에 대한 추가 쿼리가 생성됩니다. 의사 코드에서 게시된 모든 게시물의 게시 작성자를 나열한다고 가정해 보겠습니다. 게시물 세트(각 게시물에는 작성자가 있음)에서 다음과 같은 작성자 이름 목록을 얻을 수 있습니다.

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

우리는 모든 작성자가 필요하다고 모델에 알리지 않으므로 개별 Post 모델 인스턴스에서 작성자 이름을 얻습니다. 매번 별도의 쿼리가 발생합니다.

Preloading

앞서 언급했듯이 ORM은 연결 로드에 대해 "게으르다". 연결된 모델 데이터를 사용하려는 경우 즉시 로딩을 사용하여 101개의 쿼리를 2개의 쿼리로 줄일 수 있습니다. 모델에 로드할 내용을 알려주기만 하면 됩니다.

다음은 사전 로드를 사용하는 Rails Active Record 가이드의 예입니다. 보시다시피, 이 개념은 Laravel의 Eager 로딩 개념과 매우 유사합니다.

# Rails
posts = Post.includes(:author).limit(100)
# Laravel
$posts = Post::with('author')->limit(100)->get();
로그인 후 복사

더 넓은 관점에서 탐구함으로써 더 나은 이해를 얻을 수 있다는 것을 알았습니다. Active Record 문서에는 아이디어가 공감되는 데 도움이 될 수 있는 몇 가지 예가 나와 있습니다.

Laravel의 Eloquent ORM

Eloquent라고 불리는 Laravel의 ORM은 모델을 쉽게 사전 로드할 수 있으며 심지어 중첩된 관계형 모델도 사전 로드할 수 있습니다. Laravel 프로젝트에서 Eager Loading을 사용하는 방법을 배우기 위해 Post 모델을 예로 들어보겠습니다.

이 프로젝트로 빌드한 다음 몇 가지 사전 로드 예제를 자세히 살펴보고 마무리하겠습니다.

Build

사전 로딩을 경험하기 위해 몇 가지 데이터베이스 마이그레이션, 모델 및 데이터베이스 시드를 구축해 보겠습니다. 따라가려면 데이터베이스에 대한 액세스 권한이 있고 기본 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 플래그는 테이블 스키마를 생성하는 데 사용될 모델과 함께 사용할 마이그레이션을 생성합니다.

데이터 모델에는 다음과 같은 연결이 있습니다.

Author -> hasMany -> hasOne ->

Let 각 데이터 테이블에 대해 단순화된 테이블 구조를 만듭니다. Laravel이 새 테이블에 대해 down() 메서드를 자동으로 추가하므로 up() 메서드만 추가했습니다. 이러한 마이그레이션 파일은 데이터베이스/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;);
    }
}
로그인 후 복사

Models

모델 연결을 정의하고 Eager 로딩으로 더 많은 실험을 수행해야 합니다. 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);
    }
}
로그인 후 복사

다음으로 appAuthor.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 파일을 열고 다음 세 가지 팩토리 모델을 기존 사용자 팩토리 모델 파일에 추가하세요.

/** @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으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!