懶載入、預載入、with()、load() 傻傻分不清楚?

MArtian發表於2022-03-25

在本文中,我們將瞭解 Laravel Eloquent 中的懶載入和預載入以及它如何在後臺執行。

Eloquent 模型關係

Factories 工廠表 與 Workers 工人表,是一對多關係。

// Factory.php
class Factory extends Model
{
    public function workers()
    {
        return $this->hasMany(Worker::class);
    }
}

// Worker.php
class Worker extends Model
{
    public function factory()
    {  
        return $this->belongsTo(Factory::class);
    }
}

當我們在控制器中請求時:

public function index()
{
    $factories = Factory::query()->find(1);
} 

此時執行的 SQL 語句只有一條

select * from `factories` where `factories`.`id` = '1'

當我們要訪問 Factory 1 的工人時:

public function index()
{
    $factories = Factory::query()->find(1);
    $factories->workers;
} 

此時執行的 SQL 語句是

select * from `factories` where `factories`.`id` = '1'
select * from `workers` where `workers`.`factory_id` = '1' 

產生了一次額外查詢。

因為 Eloquent 僅處理了對 Factory 模型進行了查詢,並不知道你想要 workers 的關聯資料,所以並沒有為你準備好它,這樣可以避免不必要的查詢,來加快返回效率。

Laravel 中對所有模型關聯關係的訪問,如果沒有使用 with() 提前告訴 Eloquent 你想要關聯的關係,從而進行訪問時,就叫 懶載入。通常也是 N+1 問題經常會出現的地方。

假如我們要訪問所有工廠的工人呢?

public function index()
{
    $factories = Factory::query()->get(); // 工廠表有 10 條記錄
    foreach($factories as $factory)
    { 
        $factories->workers;
    }
} 

這時就會產生 N+1 的的問題,看一下 SQL 語句

select * from `workers` where `workers`.`factory_id` = '1'
select * from `workers` where `workers`.`factory_id` = '2'
...
select * from `workers` where `workers`.`factory_id` = '9'
select * from `workers` where `workers`.`factory_id` = '10

產生了 10 次 SQL 查詢,加上本身對所有工廠的查詢,一共 11 次,這就是 N+1 了。


我為什麼用 Facotry 工廠表 和 Workers 工人表來舉例呢?因為我要用更直白的話語來描述。

工廠晚上 5 點下班,工人們都回宿舍休息了。晚上 10 點時,流水線長突然接到上頭指示,來了個急活,需要工人加班來工作。因為工人並不知道晚上要加班,所以都脫衣服上床睡覺了,這個時候線長是不是要把他們挨個都叫起來呀?工人從被窩起來,打著哈欠,一邊穿衣服一邊嘴裡罵罵咧咧,然後回到生產線幹活,這個過程就是 `懶載入`

其實 懶載入 並沒有什麼壞處,它在執行效率上是最優的。程式只查詢預期中的的資料,並不知道你要訪問它的模型關係,當你需要訪問模型關係時,再去查詢一次就好了。

但是我們需要注意的是,當查詢結果不是一個單條記錄(Model),而是多條記錄(Collection)時,如果這個時候要去訪問 Collection 中每條記錄的模型關係,那就需要使用接下來的 預載入 了。否則就會產生上文的 N+1 的問題。


還是剛才的查詢,這次使用 with() 預載入工人關係。

public function index()
{
    $factories = Factory::query()->with('workers')->get(); // 工廠表有 10 條記錄
    foreach($factories as $factory)
    {
        $factory->workers;
    }
}

此時 SQL 查詢就只有 2 條。

select * from `factories`
select * from `workers` where `workers`.`factory_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

還是上面的工廠例子,再舉一次:

工廠接到一筆新訂單,是個急活,廠長週一就通知下去本週六加班,所有工人必須守候在流水線隨時待命,這就叫`預載入`

工人資訊已經準備好了,只等著你去訪問它就可以了。


由此可以得出結論:

當你的查詢結果返回的是一個 單條記錄(Model),此時 懶載入預載入 其實沒有區別,因為每一個模型關係就是一次查詢,所以這裡不論是使用 with() 預載入,還是直接使用 懶載入,對於單條記錄的模型來說,最終 SQL 執行條數都是一樣的。

但當你的查詢結果返回的是多條記錄(Collection) 時,如果要訪問模型關係,就必須使用 with() 預載入,否則就會產生 N+1 問題。


那麼 load() 是幹嘛的?

我們這樣查詢可以嗎?

public function index()
{
    $factory = Factory::query()->load('workers')->find(1);
}

結果是肯定不行的

BadMethodCallException: Call to undefined method Illuminate\Database\Eloquent\Builder::load()

在一個查詢沒有使用 get()find() 返回之前,它都是一個 EloquentBuilder 物件,我們的所有 where()with()whereIn()、等方法都是在構造查詢語句,但其實並沒有資料被真正的查詢。

當這條語句被執行時,並沒有 SQL 語句被執行。

Factory::query()->with('workers');

load() 是模型 Model 才能使用的方法, EloquentBuilder 是不能使用的。


看一下如何使用 load()

public function index()
{
    $factory = Factory::query()->find(1);
    $factory->load('workers'); 
}

此時被執行的 SQL 語句是:

select * from `factories` where `factories`.`id` = '1' limit 1
select * from `workers` where `workers`.`factory_id` in (1)

其實上面的查詢和下面的這句 with()是一模一樣的:

public function index()
{
    $factory = Factory::query()->with('workers')->find(1); 
}

那麼 load() 的使用場景是?

假如我們使用依賴注入的方式來查詢 Factory,但同時我們還要把 workers 關聯一併返回的時候,就會用到它了。

public function index(Factory $factory)
{ 
    $factory->load('workers');
}

with() 是查詢時一併載入模型關聯,load() 是先有模型被查詢後,再載入模型的關聯時使用的。


希望本篇文章能幫你理清這些概念,如果我有哪裡表達不清楚的,還請指正。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
我從未見過一個早起、勤奮、謹慎,誠實的人抱怨命運。

相關文章