使用 Eager Loading 优化 Laravel Eloquent 查询

发布于 作者:

Optimize Laravel Eloquent Queries with Eager Loading image

对象关系映射 (ORM) 使数据库操作变得异常简单。虽然以面向对象的方式定义数据库关系使查询相关模型数据变得容易,但开发人员可能不会注意到底层的数据库调用。

什么是 Eager Loading?

本质上,Eager Loading 是告诉 Eloquent 你想要使用 with 获取特定关系的模型,这样框架会生成一个更有效的查询来获取你需要的所有数据。通过 Eager Loading,你可以将多个查询减少到一到两个。

在本教程中,我们将设置一些关系示例,然后逐步介绍带有和不带有 Eager Loading 的查询是如何变化的。我喜欢直接接触代码并进行实验,我希望通过一些示例来阐明 Eager Loading 的工作原理,这些示例将进一步帮助你理解如何优化查询。

简介

在基本层面上,ORM 会“延迟”加载相关模型数据。毕竟,ORM 如何知道你的意图?也许你永远不会在查询模型之后实际使用相关模型的数据。不优化查询被称为“N+1”问题。当你使用对象来表示查询时,你可能是在不知情的情况下进行查询。

想象一下,你从数据库中收到了 100 个对象,每个记录都与 1 个关联模型相关联(即 belongsTo)。默认情况下,使用 ORM 会产生 101 个查询;一个查询用于最初的 100 个记录,以及在访问模型对象上的相关数据时,每个记录都会进行额外查询。在伪代码中,假设你想要列出所有发布过文章的作者。从文章集合(每篇文章有一个作者)中,你可以这样获取作者列表

$posts = Post::published()->get(); // one query
 
$authors = array_map(function($post) {
// Produces a query on the author model
return $post->author->name;
}, $posts);

我们没有告诉模型我们需要所有作者,因此每次从单个 Post 模型实例中获取作者姓名时,都会执行一个单独的查询。

Eager Loading

正如我所说,ORM 会“延迟”加载关联。如果你打算使用关联模型数据,可以使用 Eager Loading 将 101 个查询总数缩减到 2 个查询。你只需要告诉模型你需要它积极加载的内容。

以下来自 Rails Active Record 指南 中关于使用 Eager Loading 的示例。正如你所看到的,这个概念与 Laravel 的 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 项目中使用 Eager Loading。

我们将使用项目设置,然后深入研究一些 Eager Loading 示例,以便总结。

设置

让我们设置一些 数据库迁移、模型和 数据库播种,以便进行 Eager Loading 实验。如果你想跟着做,我假设你可以访问数据库,并且可以完成基本的 Laravel 安装

使用 Laravel 安装程序,让我们创建项目

laravel new blog-example

编辑你的 .env 值以匹配你的数据库或选择。

接下来,我们将创建三个模型,以便你可以对嵌套关系进行 Eager Loading 实验。这个示例很简单,因此我们可以专注于 Eager Loading,并且省略了你可能使用的内容,例如索引和外键约束。

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
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('author_id');
$table->string('title');
$table->text('body');
$table->timestamps();
});
}
 
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('posts');
}
}
<?php
 
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
 
class CreateAuthorsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('authors', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->text('bio');
$table->timestamps();
});
}
 
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('authors');
}
}
<?php
 
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
 
class CreateProfilesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('profiles', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('author_id');
$table->date('birthday');
$table->string('city');
$table->string('state');
$table->string('website');
$table->timestamps();
});
}
 
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('profiles');
}
}

模型

你需要定义模型关联才能进行 Eager Loading 实验。当你运行 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);
}
}

模型和迁移就位后,你可以运行迁移,然后继续使用一些播种模型数据来进行 Eager Loading 实验。

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 [
'title' => $faker->sentence,
'author_id' => function () {
return factory(App\Author::class)->create()->id;
},
'body' => $faker->paragraphs(rand(3,10), true),
];
});
 
/** @var \Illuminate\Database\Eloquent\Factory $factory */
$factory->define(App\Author::class, function (Faker\Generator $faker) {
return [
'name' => $faker->name,
'bio' => $faker->paragraph,
];
});
 
$factory->define(App\Profile::class, function (Faker\Generator $faker) {
return [
'birthday' => $faker->dateTimeBetween('-100 years', '-18 years'),
'author_id' => function () {
return factory(App\Author::class)->create()->id;
},
'city' => $faker->city,
'state' => $faker->state,
'website' => $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()
);
});
}

你创建了五个作者,然后遍历每个作者,并保存一个关联的个人资料和许多文章(每个作者之间 20 到 30 篇文章)。

我们已经完成了创建迁移、模型、模型工厂和数据库播种。我们可以将所有这些结合起来,以可重复的方式重新运行迁移和数据库播种

php artisan migrate:refresh
php artisan db:seed

你现在应该有一些播种数据可以用来玩耍了,在下一节中我们将介绍。请注意,Laravel 5.5 包括一个 migrate:fresh 命令,它会删除表,而不是回滚迁移,然后重新应用它们。

Eager Loading 实验

我们终于准备好见证 Eager Loading 的实际效果了。在我看来,可视化 Eager Loading 的最佳方法是将查询记录到 storage/logs/laravel.log 文件中。

要记录数据库查询,你可以启用 MySQL 日志,或者监听来自 Eloquent 的数据库调用。要通过 Eloquent 记录查询,请将以下代码添加到 app/Providers/AppServiceProvider.phpboot() 方法中

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('author')->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(['author', 'comments'])->get();

Eloquent 中的嵌套预加载

嵌套预加载的工作原理相同。在我们的示例中,Author 模型有一个个人资料。因此,将对每个个人资料执行一个查询。

清空 laravel.log 文件,让我们试一试。

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

现在您将有七个查询。前两个是预加载的,然后每次我们获得一个新个人资料时,都需要一个查询来获取每个作者的个人资料数据。

使用预加载,我们可以避免嵌套关系中的额外查询。最后一次清空您的 laravel.log 文件,并运行以下命令。

>>> $posts = App\Post::with('author.profile')->get();
>>> $posts->map(function ($post) {
... return $post->author->profile;
... });

现在,您应该只有 3 个查询。

[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('author.profile');
>>> $posts->first()->author->profile;
...

您应该看到总共三个查询,但只有在调用 $posts->load() 时才会出现。

结论

希望您对预加载模型有了更多了解,并理解了它的工作原理。在较深的层次上。 预加载 文档是全面的,我希望额外的实践练习能帮助您在优化关系查询方面更有信心。

Paul Redmond photo

Laravel News 的特约撰稿人。全栈 Web 开发人员和作家。

Cube

Laravel 新闻

加入 40,000 多名其他开发人员,永不错过新的技巧、教程等等。

Laravel Forge logo

Laravel Forge

轻松创建和管理您的服务器,并在几秒钟内部署您的 Laravel 应用程序。

Laravel Forge
Tinkerwell logo

Tinkerwell

Laravel 开发人员必备的代码运行器。使用 AI、自动补全和对本地和生产环境的即时反馈进行调试。

Tinkerwell
No Compromises logo

绝不妥协

Joel 和 Aaron,来自 No Compromises 播客的两位经验丰富的开发人员,现在可以为您的 Laravel 项目聘用。 ⬧ 固定费率为 7500 美元/月。 ⬧ 没有冗长的销售流程。 ⬧ 没有合同。 ⬧ 100% 退款保证。

绝不妥协
Kirschbaum logo

Kirschbaum

提供创新和稳定性,确保您的 Web 应用程序取得成功。

Kirschbaum
Shift logo

Shift

运行旧版本的 Laravel?即时、自动的 Laravel 升级和代码现代化,让您的应用程序保持新鲜。

Shift
Bacancy logo

Bacancy

使用经验丰富的 Laravel 开发人员为您的项目增效,该开发人员拥有 4-6 年的经验,每月仅需 2500 美元。获得 160 小时的专业知识和 15 天的无风险试用。立即安排电话会议!

Bacancy
Lucky Media logo

Lucky Media

立即获得 Lucky - Laravel 开发的理想选择,拥有十多年的经验!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar:Laravel 电子商务

Laravel 的电子商务。一个开源软件包,将现代无头电子商务功能的力量带到 Laravel。

Lunar:Laravel 电子商务
LaraJobs logo

LaraJobs

官方 Laravel 工作板

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit:Laravel SaaS 启动工具包

SaaSykit 是一个 Laravel SaaS 启动工具包,包含运行现代 SaaS 所需的所有功能。付款、漂亮的结账、管理面板、用户仪表板、身份验证、就绪组件、统计、博客、文档等等。

SaaSykit:Laravel SaaS 启动工具包
Rector logo

Rector

您无缝升级 Laravel、降低成本和加速创新的合作伙伴,为成功企业提供服务。

Rector
MongoDB logo

MongoDB

通过 MongoDB 和 Laravel 的强大集成来增强您的 PHP 应用程序,使开发人员能够轻松高效地构建应用程序。支持事务、搜索、分析和移动用例,同时使用熟悉的 Eloquent API。了解灵活、现代的 MongoDB 数据库如何改变您的 Laravel 应用程序。

MongoDB
Maska is a Simple Zero-dependency Input Mask Library image

Maska 是一个简单的零依赖输入掩码库

阅读文章
Add Swagger UI to Your Laravel Application image

将 Swagger UI 添加到您的 Laravel 应用程序

阅读文章
Assert the Exact JSON Structure of a Response in Laravel 11.19 image

在 Laravel 11.19 中断言响应的精确 JSON 结构

阅读文章
Build SSH Apps with PHP and Laravel Prompts image

使用 PHP 和 Laravel 提示构建 SSH 应用程序

阅读文章
Building fast, fuzzy site search with Laravel and Typesense image

使用 Laravel 和 Typesense 构建快速、模糊的网站搜索

阅读文章
Add Comments to your Laravel Application with the Commenter Package image

使用 Commenter 软件包为您的 Laravel 应用程序添加评论

阅读文章