updatecounters是单行原子自增,需先通过findone()等加载实例再调用,不支持条件参数;updateallcounters才是真正的条件批量自增,直接拼sql执行,不触发事件和验证。

updateCounters 是单行原子自增,不是批量操作
updateCounters() 必须基于已加载的 ActiveRecord 实例调用,它本质是「先查后原子更新」:查出当前记录、构造 SET view_count = view_count + 1 类 SQL、执行。它不支持直接传入条件数组做无主键定位。
常见误用是写成:$post->updateCounters(['view_count' => 1], ['id' => 123]) —— 第二个参数会被忽略,updateCounters() 不接受条件参数。
- 正确姿势:先
findOne()或find()->where(...)->one()拿到模型实例,再调用updateCounters() - 如果目标行不存在,
findOne()返回null,后续调用会报错(调用 null 的方法),必须判空 - 它底层用的是
UPDATE ... SET col = col + N WHERE primary_key = ?,依赖主键锁定,天然防并发丢失更新
updateAllCounters 才是真正的条件批量自增
需要按条件(比如 status = 1)批量给多行的某个字段加减时,必须用静态方法 updateAllCounters()。它跳过模型加载,直接拼 SQL 发送到数据库。
典型错误是混淆 updateCounters() 和 updateAllCounters(),结果只更新了 1 行却以为改了一堆。
- 语法:
Article::updateAllCounters(['view_count' => 1], ['category_id' => $catId]) - 第二个参数支持完整 Query 条件写法,例如:
['and', ['status' => 1], ['>','created_at',$time]] - 它不校验数据合法性(比如字段是否为数字类型),失败时只返回影响行数,不会抛异常,需手动检查返回值
- 注意:该方法不触发模型事件(
beforeUpdate/afterUpdate)和验证规则
别在 save() 里做计数器更新
用 save() 更新计数列(如 $model->view_count++; 然后 $model->save())是高危操作。它本质是「读-改-写」三步,在并发下必然丢更新。
现象:两个请求同时读到 view_count = 100,各自加 1 后都写回 101,最终结果仍是 101 而非 102。
- 只要字段用途是计数(阅读量、点赞数、库存扣减等),就绝不能走
save() -
updateCounters()和updateAllCounters()底层都是原子 SQL,数据库保证最终一致性 - 如果业务逻辑复杂(比如要判断用户是否已点过赞才决定是否加赞),就把条件写进
WHERE,而不是靠 PHP 判断后再save()
NULL 值和负数要提前兜底
MySQL 中若字段初始为 NULL,view_count = view_count + 1 结果仍是 NULL。Yii 不自动处理这个边界,得靠建表或迁移时设默认值。
负数更新虽合法(如 ['stock' => -1]),但容易掩盖业务逻辑漏洞:比如库存扣成负数却不报警。
- 建表时给计数字段设
DEFAULT 0,避免 NULL 参与运算 - 对关键字段(如库存),建议在数据库加
CHECK (stock >= 0)约束(MySQL 8.0.16+ 支持) -
updateAllCounters()返回整数(影响行数),若为 0,说明 WHERE 没匹配到任何行——可能是条件写错,也可能是数据已被删,得结合业务判断是否要告警
updateCounters() 却传了条件,或者该用 updateAllCounters() 却去循环查模型再逐个 updateCounters() —— 后者在高并发下可能把数据库锁住。











