小心 Laravel 中的 Model::increment
Laravel v5.4.18 中的一个提交,导致的 BUG,因为添加了错误的单测,导致没办法轻易修改,这里提醒大家,使用时需要谨慎,以免采坑。
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 ORM、WebSocket 服务端及客户端、JSON RPC 服务端及客户端、GRPC 服务端及客户端、OpenTracing(Zipkin, Jaeger) 客户端、Guzzle HTTP 客户端、Elasticsearch 客户端、Consul、Nacos 服务中心、ETCD 客户端、AMQP 组件、Nats 组件、Apollo、ETCD、Zookeeper、Nacos 和阿里云 ACM 的配置中心、基于令牌桶算法的限流器、通用连接池、熔断器、Swagger 文档生成、Swoole Tracker、Blade、Smarty、Twig、Plates 和 ThinkTemplate 视图引擎、Snowflake 全局ID生成器、Prometheus 服务监控 等组件,省去了自己实现对应协程版本的麻烦。
Hyperf 还提供了 基于 PSR-11 的依赖注入容器、注解、AOP 面向切面编程、基于 PSR-15 的中间件、自定义进程、基于 PSR-14 的事件管理器、Redis/RabbitMQ 消息队列、自动模型缓存、基于 PSR-16 的缓存、Crontab 秒级定时任务、Session、i18n 国际化、Validation 表单验证 等非常便捷的功能,满足丰富的技术场景和业务场景,开箱即用。
本作品采用《CC 协议》,转载必须注明作者和本文链接
关于 LearnKu
朋友,你写了这么多真的很辛苦了,但是这个吧,我觉得它不是个bug。
increment和decrement,这个东西就不是配合save()来使用的,只要使用了这个函数就会直接生成一个field = field + 1的 SQL,上方截图文档也描述的比较清楚,如果要顺带更新其他字段也提供了方式。代码中可以 执行完
increment后,如果想用模型的需求, 可以再查询一下同步一下。你文章中的这段代码,生成了一个查询和一个更改语句,但是那条语句并不是
save产生的,因为模型值没有改变,save的时候对比没有变化,故只执行了select。@lengqy 大兄弟,咱别看翻译的文档。。。咱可以看看源码。。。
第一个问题,
increment后面跟着save的情况,本身就不合逻辑。你想要在increment的同时,更新其他字段,框架已经提供extra参数来达到这个目地了。这个时候非要再save一下,纯粹是多余操作。第二个问题,接上条,你想要在
increment的同时,更新其他字段,不用框架提供的extra参数,非要直接修改属性,修改属性了又不save,怪框架没给你更新。总结一下,该
save的不save, 不该save的时候偏要save,应该是你使用的姿势不对。============================================================================
吐槽结束,回到这个问题来看,我的观点是在
8.x以前的版本中,这不算是一个bug,但在8.x的版本中,算是一个bug。我这样说的理由,是基于increment操作的语义。模型的
fill方法,或者修改属性,会导致dirty的状态发生变化;模型的update操作,导致changed状态发生变化。那么在解决这个问题前,不妨先明确这几个问题。
increment的操作是否包含fill的语义? 从官方的实现来看,是不包含的。increment带extra的操作是否包含fill额外字段的语义? 从官方的实现来看,是包含的。increment的操作是否包含update的语义? 从官方的实现来看,8.x以前的版本中不包含,但在8.x中是包含的。increment不包含被自增的字段的fill语义,包含fill额外字段的语义,因此在increment后做save操作,dirty检查时会检查到extra的字段,所以也会执行相关SQL语句。在
8.x以前的版本中,increment不包含update的语义,所以在increment后面做changed检查没有有意义,无论检查是什么结果,都不能拿过来作为逻辑判断依据。但在
8.x的版本中,increment包含update的语义,执行increment后,应该要对changed检查结果负责。所以,我的看法是
8.x的升级导致破坏了increment操作的语义,从这个角度来看,新版本确实应该修复这个问题。