小心 Laravel 中的 Model::increment

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 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 协议》,转载必须注明作者和本文链接
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 3

file

朋友,你写了这么多真的很辛苦了,但是这个吧,我觉得它不是个bug。

increment和decrement,这个东西就不是配合save()来使用的,只要使用了这个函数就会直接生成一个 field = field + 1 的 SQL,上方截图文档也描述的比较清楚,如果要顺带更新其他字段也提供了方式。

代码中可以 执行完 increment 后,如果想用 模型 的需求, 可以再查询一下同步一下。


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

你文章中的这段代码,生成了一个查询和一个更改语句,但是那条语句并不是save产生的,因为模型值没有改变,save的时候对比没有变化,故只执行了select。

5年前 评论
李铭昕 (楼主) 5年前
李铭昕 (楼主) 5年前
Adachi (作者) 5年前
李铭昕 (楼主) 5年前
李铭昕 (楼主) 5年前
李铭昕

file

@lengqy 大兄弟,咱别看翻译的文档。。。咱可以看看源码。。。

5年前 评论
Adachi 5年前

第一个问题,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 操作的语义,从这个角度来看,新版本确实应该修复这个问题。

increment[v8.x] = increment[v5.5] + update[v5.5]
5年前 评论
slpi1 (作者) 5年前
李铭昕 (楼主) 5年前
李铭昕 (楼主) 5年前
李铭昕 (楼主) 5年前
slpi1 (作者) 5年前
李铭昕 (楼主) 5年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!