Laravel Eloquent 事件中的 Updated 和 Saved 原來不一樣

當一個新模型初次被儲存,將會觸發 creating 以及 created 事件。如果一個模型已經存在於資料庫而且呼叫了 save 方法,將會觸發 updatingupdated 事件。然而,在這兩個狀況下,都將會觸發 savingsaved 事件。
https://laravel.tw/docs/5.1/eloquent#events

Laravel 在資料更新的時候會觸發兩個事件:updated 和 saved。單從官方文件的說明,saved 看起來像是一個方便開發者統一管理新增/更新動作的事件,讓重複的程式碼可以不用被寫在兩個地方(created 和 updated),但是其實 saved 和 updated 的觸發時機是不一樣的。

直接來看一下 Laravel eloquent 的原始碼。

// 495 行
public function save(array $options = [])
{
...
if ($this->exists) {
$saved = $this->isDirty() ?
$this->performUpdate($query) : true;
}
...
if ($saved) {
$this->finishSave($options);
}
return $saved;
}
// 552 行
protected function finishSave(array $options)
{
$this->fireModelEvent('saved', false);
$this->syncOriginal();
if (Arr::get($options, 'touch', true)) {
$this->touchOwners();
}
}
// 569 行
protected function performUpdate(Builder $query)
{
...
$dirty = $this->getDirty();
if (count($dirty) > 0) {
$this->setKeysForSaveQuery($query)->update($dirty);
$this->fireModelEvent('updated', false);
}
return true;
}

應該盡量使用 updated 而不是 saved 來判斷資料是否更新

可以看到在觸發 updated 之前會先做一次 getDirty() 的檢查,其實就是在檢查這次更新的資料是不是真的有改動資料的值;相反的,saved 不管值有沒有改動都會觸發。如果沒注意到這個差別的話,這在實務上其實會造成一些問題。

Saved 無論如何都會被觸發,updated 則不是。

例如,我們通常會把資料的結果快取起來,以減少對資料庫的查詢次數。因此當資料有變動的時候,就會需要把對應的快取刪除。為了避免有漏刪的部分,會把刪除快取的動作綁定在 eloquent 事件中。認清 updated 和 saved 之後,就會知道這個動作應該是要綁在 updated;若是綁在 saved 上,就會造成多餘的快取動作,增加機器的成本。當快取數量一多,又有用到類似 redis scan 這種較耗時的功能來找出需要刪除的快取時,對於機器的效能影響更大。

使用 updated 也仍會可能有漏網之魚

但是,要注意的是,即使使用了 updated 也不能說完全避免這件事的發生。這時候又要再看一次 Larvel eloquent 的原始碼了。

// 3093 行
public function getDirty()
{
$dirty = [];
foreach ($this->attributes as $key => $value) {
if (! array_key_exists($key, $this->original)) {
$dirty[$key] = $value;
} elseif ($value !== $this->original[$key] &&
! $this->originalIsNumericallyEquivalent($key)) {
$dirty[$key] = $value;
}
}
return $dirty;
}

可以發現 Laravel 判斷值有沒有改動的比較依據來自於,我們一開始 select 出來的欄位,跟這次要變動的欄位。意思就是,如果我們只 select 了 A 欄位出來,但是更新的是 B 欄位時,即使更動後 B 欄位的值跟更動之前一模一樣,Laravel 仍會把此視為資料的值有所改動。

即使值沒改變,updated 也仍舊會在此情況下被觸發。

不過這仍在可以接受的範圍之內。畢竟每個更新之前,若要為了拿到所有欄位而又再多查詢一次資料庫,可能會是一件更浪費資源的事情。