小心 Laravel 中的 Model::increment

李銘昕發表於2020-12-30

Laravel v5.4.18 中的一個提交,導致的 BUG,因為新增了錯誤的單測,導致沒辦法輕易修改,這裡提醒大家,使用時需要謹慎,以免採坑。

commit

pr#35748

BUG 重現

1. increment extra 後再進行 save 操作,會執行兩句SQL

我們先寫一段沒有 extra 資料的程式碼,進行測試

DB::enableQueryLog();
$model = UserExt::query()->find(101);
$model->increment('count');
$model->save();
dump(DB::getQueryLog());

透過測試得知,以上程式碼只會生成兩段 SQL,分別是

select * from `user_ext` where `user_ext`.`id` = ? limit 1
update `user_ext` set `count` = `count` + 1, `user_ext`.`updated_at` = ? where `id` = ?

然後讓我們修改測試程式碼

DB::enableQueryLog();
$model = UserExt::query()->find(101);
$model->increment('count', 1, [
    'str' => uniqid()
]);
$model->save();
dump(DB::getQueryLog());

這時,會生成以下三段 SQL

select * from `user_ext` where `user_ext`.`id` = ? limit 1
update `user_ext` set `count` = `count` + 1, `str` = ?, `user_ext`.`updated_at` = ? where `id` = ?
update `user_ext` set `str` = ?, `user_ext`.`updated_at` = ? where `id` = ?

且第二段和第三段 SQL 中,str 的值是一致的。這個問題的主要原因,便是 extra 裡的資料不會被同步到 original 中,就導致第二次 save 計算 dirty 的時候,出現了BUG。

2. getChanges 表現不一致

經過第一個 BUG 的重現,那麼第二個問題也就很容易想到了,就是 getChanges 方法。

讓我們繼續編寫程式碼測試

$model = UserExt::query()->find(101);
$model->increment('count');
dump($model->getChanges());

以上程式碼會輸出以下資料,可見還是符合預期的

array:1 ["count" => 4
]

讓我們繼續修改程式碼,在 increment 前增加一次賦值

DB::enableQueryLog();
$model = UserExt::query()->find(101);
$model->str = uniqid();
$model->increment('count');
dump($model->getChanges());
dump(DB::getQueryLog());

會得到以下輸出

array:2 ["count" => 7
  "str" => "5febf2dc798ed"
]

看似沒有問題,但讓我們檢查一下 SQL

select * from `user_ext` where `user_ext`.`id` = ? limit 1
update `user_ext` set `count` = `count` + 1, `user_ext`.`updated_at` = ? where `id` = ?

卻發現,並沒有修改 str 的資料,那顯然 getChanges 與預期不符。

實際上,increment 在設計上,並沒有想要修改前面 setter 的資料,但這種情況下,我們 getChanges 便也不能把 str 算進來。

讓我們繼續修改程式碼

DB::enableQueryLog();
$model = UserExt::query()->find(101);
$model->str = uniqid();
$model->increment('count');
dump($model->getChanges());
$model->save();
dump($model->getChanges());
dump(DB::getQueryLog());

兩次 getChanges 輸出如下

array:2 ["count" => 9
  "str" => "5febf3d6418e8"
]
array:2 ["str" => "5febf3d6418e8"
  "updated_at" => "2020-12-30 03:28:22"
]

可見兩次 getChanges 中,str 的值是一致的。

save 的時候會把 updated_at 算進來,而 increment 的時候是不會算 updated_at,這裡至少行為一致,可以作為後續的最佳化項。

輸出的 SQL 如下

select * from `user_ext` where `user_ext`.`id` = ? limit 1
update `user_ext` set `count` = `count` + 1, `str` = ?, `user_ext`.`updated_at` = ? where `id` = ?
update `user_ext` set `str` = ?, `user_ext`.`updated_at` = ? where `id` = ?

Hyperf 是基於 Swoole 4.5+ 實現的高效能、高靈活性的 PHP 協程框架,內建協程伺服器及大量常用的元件,效能較傳統基於 PHP-FPM 的框架有質的提升,提供超高效能的同時,也保持著極其靈活的可擴充套件性,標準元件均基於 PSR 標準 實現,基於強大的依賴注入設計,保證了絕大部分元件或類都是 可替換可複用 的。

框架元件庫除了常見的協程版的 MySQL 客戶端Redis 客戶端,還為您準備了協程版的 Eloquent ORMWebSocket 服務端及客戶端JSON RPC 服務端及客戶端GRPC 服務端及客戶端OpenTracing(Zipkin, Jaeger) 客戶端Guzzle HTTP 客戶端Elasticsearch 客戶端Consul、Nacos 服務中心ETCD 客戶端AMQP 元件Nats 元件Apollo、ETCD、Zookeeper、Nacos 和阿里雲 ACM 的配置中心基於令牌桶演算法的限流器通用連線池熔斷器Swagger 文件生成Swoole TrackerBlade、Smarty、Twig、Plates 和 ThinkTemplate 檢視引擎Snowflake 全域性ID生成器Prometheus 服務監控 等元件,省去了自己實現對應協程版本的麻煩。

Hyperf 還提供了 基於 PSR-11 的依賴注入容器註解AOP 面向切面程式設計基於 PSR-15 的中介軟體自定義程式基於 PSR-14 的事件管理器Redis/RabbitMQ 訊息佇列自動模型快取基於 PSR-16 的快取Crontab 秒級定時任務Sessioni18n 國際化Validation 表單驗證 等非常便捷的功能,滿足豐富的技術場景和業務場景,開箱即用。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.

相關文章