對(duì)象關(guān)系映射(ORM)使得處理數(shù)據(jù)驚人地簡(jiǎn)單。由于以面向?qū)ο?/b>的方式定義數(shù)據(jù)之間關(guān)系使得查詢關(guān)聯(lián)模型數(shù)據(jù)變得容易,開(kāi)發(fā)者不太需要關(guān)注數(shù)據(jù)底層調(diào)用。
ORM 的標(biāo)準(zhǔn)數(shù)據(jù)優(yōu)化是渴望式加載相關(guān)數(shù)據(jù)。我們將建立一些示例關(guān)系,然后逐步了解查詢隨著渴望式加載和非渴望式加載變化。我喜歡直接使用代碼來(lái)試驗(yàn)一些東西,并通過(guò)一些示例來(lái)說(shuō)明渴望式加載是如何工作的,這將進(jìn)一步幫助你理解如何優(yōu)化查詢。
介紹
在基本級(jí)別,ORM 是 “懶惰” 加載相關(guān)的模型數(shù)據(jù)。但是,ORM 應(yīng)該如何知道你的意圖?在查詢模型后,您可能永遠(yuǎn)不會(huì)真正使用相關(guān)模型的數(shù)據(jù)。不優(yōu)化查詢被稱為 “N + 1” 問(wèn)題。當(dāng)您使用對(duì)象來(lái)表示查詢時(shí),您可能在不知情的情況下進(jìn)行查詢。
想象一下,您收到了 100 個(gè)來(lái)自數(shù)據(jù)庫(kù)的對(duì)象,并且每條記錄都有 1 個(gè)關(guān)聯(lián)的模型(即 belongsTo)。使用 ORM 默認(rèn)會(huì)產(chǎn)生 101 條查詢;對(duì)原始 100 條記錄 進(jìn)行一次查詢,如果訪問(wèn)了模型對(duì)象上的相關(guān)數(shù)據(jù),則對(duì)每條記錄進(jìn)行附加查詢。在偽代碼中,假設(shè)您要列出所有已發(fā)布帖子的發(fā)布作者。從一組帖子(每個(gè)帖子有一位作者),您可以得到一個(gè)作者姓名列表,如下所示:
$posts?=?Post::published()->get();?//?一次查詢 $authors?=?array_map(function($post)?{ ????//?生成對(duì)作者模型的查詢 ????return?$post->author->name; },?$posts);
我們并沒(méi)有告訴模型我們需要所有作者,因此每次從各個(gè) Post 模型實(shí)例中獲取作者姓名時(shí)都會(huì)發(fā)生單獨(dú)的查詢 。
預(yù)加載
正如我所提到的,ORM 是 “懶惰” 加載關(guān)聯(lián)。如果您打算使用關(guān)聯(lián)的模型數(shù)據(jù),則可以使用預(yù)加載將 101 次查詢縮減為 2 次查詢。您只需要告訴模型你渴望它加載什么。
以下是使用預(yù)加載的 Rails Active Record guide 中的示例.。正如您所看到的,這個(gè)概念與 laravel’s eager loading 概念非常相似。
#?Rails posts?=?Post.includes(:author).limit(100) #?Laravel $posts?=?Post::with('author')->limit(100)->get();
通過(guò)從更廣闊的視角探索,我發(fā)現(xiàn)我獲得了更好的理解。Active Record 文檔涵蓋了一些可以進(jìn)一步幫助該想法產(chǎn)生共鳴的示例。
Laravel 的 Eloquent ORM
Laravel 的 ORM,叫作 Eloquent, 可以很輕松的預(yù)加載模型,甚至預(yù)加載嵌套關(guān)聯(lián)模型。讓我們以 Post 模型為例,學(xué)習(xí)如何在 Laravel 項(xiàng)目中使用預(yù)先加載。
我們將使用這個(gè)項(xiàng)目構(gòu)建,然后更深入地瀏覽一些預(yù)加載示例以進(jìn)行總結(jié)。
構(gòu)建
讓我們構(gòu)建一些 數(shù)據(jù)庫(kù)遷移, 模型, 和? 數(shù)據(jù)庫(kù)種子 來(lái)體驗(yàn)預(yù)加載。如果你想跟著操作,我假設(shè)你有權(quán)訪問(wèn)數(shù)據(jù)庫(kù)并且可以完成了基本的 Laravel 安裝。
使用 Laravel 安裝器,新建項(xiàng)目:
laravel?new?blog-example
根據(jù)你的數(shù)據(jù)庫(kù)和選擇編輯 .env 文件。
接下來(lái),我們將創(chuàng)建三個(gè)模型,以便您可以嘗試預(yù)加載嵌套關(guān)系。這個(gè)例子很簡(jiǎn)單,所以我們可以專注于預(yù)加載,而且我省略了你可能會(huì)使用的東西,如索引和外鍵約束。
php?artisan?make:model?-m?Post php?artisan?make:model?-m?Author php?artisan?make:model?-m?Profile
該 -m 標(biāo)志創(chuàng)建一個(gè)遷移,以與將用于創(chuàng)建表模式的模型一起使用。
數(shù)據(jù)模型將具有以下關(guān)聯(lián):
Post -> belongsTo -> Author
Author -> hasMany -> Post
Author -> hasOne -> Profile
遷移
讓我們?yōu)槊總€(gè)數(shù)據(jù)表創(chuàng)建一個(gè)簡(jiǎn)表結(jié)構(gòu)。我只添加了 up() 方法,因?yàn)?Laravel 將會(huì)為新的數(shù)據(jù)表自動(dòng)添加 down() 方法。這些遷移文件放在了 database/migrations/ 目錄中:
<?php use IlluminateSupportFacadesSchema; use IlluminateDatabaseSchemaBlueprint; use IlluminateDatabaseMigrationsMigration; class CreatePostsTable extends Migration { /** * 執(zhí)行遷移 * * @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(); ????????}); ????} ????/** ?????*?回滾遷移 ?????* ?????*?@return?void ?????*/ ????public?function?down() ????{ ????????Schema::dropIfExists('posts'); ????} }
<?php use IlluminateSupportFacadesSchema; use IlluminateDatabaseSchemaBlueprint; use IlluminateDatabaseMigrationsMigration; class CreateAuthorsTable extends Migration { /** * 執(zhí)行遷移 * * @return void */ public function up() { Schema::create('authors', function (Blueprint $table) { $table->increments('id'); ????????????$table->string('name'); ????????????$table->text('bio'); ????????????$table->timestamps(); ????????}); ????} ????/** ?????*?回滾遷移 ?????* ?????*?@return?void ?????*/ ????public?function?down() ????{ ????????Schema::dropIfExists('authors'); ????} }
<?php use IlluminateSupportFacadesSchema; use IlluminateDatabaseSchemaBlueprint; use IlluminateDatabaseMigrationsMigration; class CreateProfilesTable extends Migration { /** * 執(zhí)行遷移 * * @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(); ????????}); ????} ????/** ?????*?回滾遷移 ?????* ?????*?@return?void ?????*/ ????public?function?down() ????{ ????????Schema::dropIfExists('profiles'); ????} }
模型
你需要定義模型關(guān)聯(lián)并通過(guò)預(yù)先加載來(lái)進(jìn)行更多的實(shí)驗(yàn)。當(dāng)你運(yùn)行 php artisan make:model 命令的時(shí)候,它將為你創(chuàng)建模型文件。
第一個(gè)模型為 app/Post.php :
<?php namespace App; use IlluminateDatabaseEloquentModel; class Post extends Model { public function author() { return $this->belongsTo(Author::class); ????} }
接下來(lái), appAuthor.php 模型有兩個(gè)關(guān)聯(lián)關(guān)系:
<?php namespace App; use IlluminateDatabaseEloquentModel; class Author extends Model { public function profile() { return $this->hasOne(Profile::class); ????} ????public?function?posts() ????{ ????????return?$this->hasMany(Post::class); ????} }
通過(guò)模型和遷移,你可以運(yùn)行遷移并繼續(xù)嘗試使用一些種子模型數(shù)據(jù)進(jìn)行預(yù)加載。
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
如果你查看下數(shù)據(jù)庫(kù),你就會(huì)看到所有已經(jīng)創(chuàng)建好的數(shù)據(jù)表!
工廠模型
為了讓我們可以運(yùn)行查詢語(yǔ)句,我們需要?jiǎng)?chuàng)建一些假數(shù)據(jù)來(lái)提供查詢,讓我們添加一些工廠模型,使用這些模型來(lái)為數(shù)據(jù)庫(kù)提供測(cè)試數(shù)據(jù)。
打開(kāi) database/factories/ModelFactory.php 文件并且將如下三個(gè)工廠模型添加到現(xiàn)有的 User 工廠模型文件中:
/**?@var?IlluminateDatabaseEloquentFactory?$factory?*/ $factory->define(AppPost::class,?function?(FakerGenerator?$faker)?{ ????return?[ ????????'title'?=>?$faker->sentence, ????????'author_id'?=>?function?()?{ ????????????return?factory(AppAuthor::class)->create()->id; ????????}, ????????'body'?=>?$faker->paragraphs(rand(3,10),?true), ????]; }); /**?@var?IlluminateDatabaseEloquentFactory?$factory?*/ $factory->define(AppAuthor::class,?function?(FakerGenerator?$faker)?{ ????return?[ ????????'name'?=>?$faker->name, ????????'bio'?=>?$faker->paragraph, ????]; }); $factory->define(AppProfile::class,?function?(FakerGenerator?$faker)?{ ????return?[ ????????'birthday'?=>?$faker->dateTimeBetween('-100?years',?'-18?years'), ????????'author_id'?=>?function?()?{ ????????????return?factory(AppAuthor::class)->create()->id; ????????}, ????????'city'?=>?$faker->city, ????????'state'?=>?$faker->state, ????????'website'?=>?$faker->domainName, ????]; });
這些工廠模型可以很容易的填充一些我們可以用來(lái)查詢的數(shù)據(jù);我們也可以使用它們來(lái)創(chuàng)建并生成關(guān)聯(lián)模型所需的數(shù)據(jù)。
打開(kāi) database/seeds/DatabaseSeeder.php 文件將以下內(nèi)容添加到 DatabaseSeeder::run() 方法中:
public?function?run() { ????$authors?=?factory(AppAuthor::class,?5)->create(); ????$authors->each(function?($author)?{ ????????$author ????????????->profile() ????????????->save(factory(AppProfile::class)->make()); ????????$author ????????????->posts() ????????????->saveMany( ????????????????factory(AppPost::class,?rand(20,30))->make() ????????????); ????}); }
你創(chuàng)建了五個(gè) author 并遍歷循環(huán)每一個(gè) author ,創(chuàng)建和保存了每個(gè) author 相關(guān)聯(lián)的 profile 和 posts (每個(gè) author 的 posts 的數(shù)量在 20 和 30 個(gè)之間)。
我們已經(jīng)完成了遷移、模型、工廠模型和數(shù)據(jù)庫(kù)填充的創(chuàng)建工作,將它們組合起來(lái)可以以重復(fù)的方式重新運(yùn)行遷移和數(shù)據(jù)庫(kù)填充:
php?artisan?migrate:refresh php?artisan?db:seed
你現(xiàn)在應(yīng)該有一些已經(jīng)填充的數(shù)據(jù),可以在下一章節(jié)使用它們。注意在 Laravel 5.5 版本中包含一個(gè) migrate:fresh 命令,它會(huì)刪除表,而不是回滾遷移并重新應(yīng)用它們。
嘗試使用預(yù)加載
現(xiàn)在我們的前期工作終于已經(jīng)完成了。 我個(gè)人認(rèn)為最好的可視化方式就是將查詢結(jié)果記錄到 storage/logs/laravel.log 文件當(dāng)中查看。
要把查詢結(jié)果記錄到日志中,有兩種方式。第一種,可以開(kāi)啟 mysql 的日志文件,第二種,則是使用 Eloquent 的數(shù)據(jù)庫(kù)調(diào)用來(lái)實(shí)現(xiàn)。通過(guò) Eloquent 來(lái)實(shí)現(xiàn)記錄查詢語(yǔ)句的話,可以將下面的代碼添加到 app/Providers/AppServiceProvider.php boot () 方法當(dāng)中:
namespace?AppProviders; use?DB; use?Log; use?IlluminateSupportServiceProvider; 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 ????????????); ????????}); ????} ????//?... }
我喜歡把這個(gè)監(jiān)聽(tīng)器封裝在配置檢查的時(shí)候,以便可以控制記錄查詢?nèi)罩镜拈_(kāi)關(guān)。你也可以從 Laravel Debugbar 獲取到更多相關(guān)的信息。
首先,嘗試一下在不使用預(yù)加載模型的時(shí)候,會(huì)發(fā)生什么情況。清除你的 storage/log/laravel.log 文件當(dāng)中的內(nèi)容然后運(yùn)行 “tinker” 命令:
php?artisan?tinker >>>?$posts?=?AppPost::all(); >>>?$posts->map(function?($post)?{ ...?????return?$post->author; ...?}); >>>?...
這個(gè)時(shí)候檢查你的 laravel.log 文件,你會(huì)發(fā)現(xiàn)一堆查詢作者的查詢語(yǔ)句:
[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() 方法來(lái)用預(yù)加載查詢作者信息:
php?artisan?tinker >>>?$posts?=?AppPost::with('author')->get(); >>>?$posts->map(function?($post)?{ ...?????return?$post->author; ...?}); ...
這次你應(yīng)該看到了,只有兩條查詢語(yǔ)句。一條是對(duì)所有帖子進(jìn)行查詢,以及對(duì)帖子所關(guān)聯(lián)的作者進(jìn)行查詢:
[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]
如果你有多個(gè)關(guān)聯(lián)的模型,你可以使用一個(gè)數(shù)組進(jìn)行預(yù)加載的實(shí)現(xiàn):
$posts?=?AppPost::with(['author',?'comments'])->get();
在 Eloquent 中嵌套預(yù)加載
嵌套預(yù)加載來(lái)做相同的工作。在我們的例子中,每個(gè)作者的 model 都有一個(gè)關(guān)聯(lián)的個(gè)人簡(jiǎn)介。因此,我們將針對(duì)每個(gè)個(gè)人簡(jiǎn)介來(lái)進(jìn)行查詢。
清空 laravel.log 文件,來(lái)做一次嘗試:
php?artisan?tinker >>>?$posts?=?AppPost::with('author')->get(); >>>?$posts->map(function?($post)?{ ...?????return?$post->author->profile; ...?}); ...
你現(xiàn)在可以看到七個(gè)查詢語(yǔ)句,前兩個(gè)是預(yù)加載的結(jié)果。然后,我們每次獲取一個(gè)新的個(gè)人簡(jiǎn)介時(shí),就需要來(lái)查詢所有作者的個(gè)人簡(jiǎn)介。
通過(guò)預(yù)加載,我們可以避免嵌套在模型關(guān)聯(lián)中的額外的查詢。最后一次清空 laravel.log 文件并運(yùn)行一下命令:
>>>?$posts?=?AppPost::with('author.profile')->get(); >>>?$posts->map(function?($post)?{ ...?????return?$post->author->profile; ...?});
現(xiàn)在,總共有三個(gè)查詢語(yǔ)句:
[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]
懶人預(yù)加載
你可能只需要收集關(guān)聯(lián)模型的一些基礎(chǔ)的條件。在這種情況下,可以懶惰地調(diào)用關(guān)聯(lián)數(shù)據(jù)的一些其他查詢:
php?artisan?tinker >>>?$posts?=?AppPost::all(); ... >>>?$posts->load('author.profile'); >>>?$posts->first()->author->profile; ...
你應(yīng)該只能看到三條查詢,并且是在調(diào)用 $posts->load() 方法后。
總結(jié)
希望你能了解到更多關(guān)于預(yù)加載模型的相關(guān)知識(shí),并且了解它是如何在更加深入底層的工作方式。 預(yù)加載文檔 是非常全面的,我希望額外的一些代碼實(shí)現(xiàn)可以幫助您更好的優(yōu)化關(guān)聯(lián)查詢。