使用 Eager Loading 优化 Laravel Eloquent 查询
发布于 作者: Paul Redmond
对象关系映射 (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 概念非常相似。
# Railsposts = 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 Postphp artisan make:model -m Authorphp artisan make:model -m Profile
-m
标志会创建一个与模型一起使用的迁移,用于创建表模式。
数据模型将具有以下关联
Post -> belongsTo -> AuthorAuthor -> hasMany -> PostAuthor -> 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 migrateMigration table created successfully.Migrating: 2014_10_12_000000_create_users_tableMigrated: 2014_10_12_000000_create_users_tableMigrating: 2014_10_12_100000_create_password_resets_tableMigrated: 2014_10_12_100000_create_password_resets_tableMigrating: 2017_08_04_042509_create_posts_tableMigrated: 2017_08_04_042509_create_posts_tableMigrating: 2017_08_04_042516_create_authors_tableMigrated: 2017_08_04_042516_create_authors_tableMigrating: 2017_08_04_044554_create_profiles_tableMigrated: 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:refreshphp artisan db:seed
你现在应该有一些播种数据可以用来玩耍了,在下一节中我们将介绍。请注意,Laravel 5.5 包括一个 migrate:fresh 命令,它会删除表,而不是回滚迁移,然后重新应用它们。
Eager Loading 实验
我们终于准备好见证 Eager Loading 的实际效果了。在我看来,可视化 Eager Loading 的最佳方法是将查询记录到 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('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()
时才会出现。
结论
希望您对预加载模型有了更多了解,并理解了它的工作原理。在较深的层次上。 预加载 文档是全面的,我希望额外的实践练习能帮助您在优化关系查询方面更有信心。