ThinkPHP 的 inc()/dec() 不立即生效,因仅预编译表达式并存入 set 数组,需调用 save()/update()/setInc() 等触发方法才执行 SQL;若模型无有效主键或事务中遗漏 save(),更新将静默失败。

ThinkPHP 的 inc() 和 dec() 为什么不是立刻生效?
因为它们默认不执行 SQL,只做表达式预编译。调用 inc('score') 后,TP 并没发 UPDATE 请求,而是把 score = score + 1 存进待执行的 set 数组里,等真正调用 save()、update() 或 setInc() 这类触发方法时才拼 SQL 发出去。
常见错误现象:$user->inc('login_count')->save(); 看似连贯,但若模型没主键或主键没设好,save() 会静默失败,字段根本没变;更隐蔽的是在事务里多次调用 inc() 却忘了最后 save(),结果全丢进黑洞。
- 必须确保模型已绑定有效主键(
$pk已定义且数据中存在),否则save()无法生成 WHERE 条件 - 不要混用
inc()和手动赋值:比如先$model->score = 100,再$model->inc('score'),后者会覆盖前者,最终只执行自增 - 批量操作慎用:在循环里反复
inc()+save()是 N 次查询;应改用Db::table()->where()->inc('field')->update()一次完成
想延迟更新但又不想依赖模型实例?用 Db::raw() + update()
当你要绕过模型层、直接对某条记录做原子自增,又希望逻辑清晰可复用,Db::raw() 是最稳的选择。它把表达式原样塞进 SQL,不经过模型属性映射,也不存在“忘记 save”的问题。
使用场景:计数器更新、库存扣减、积分变动等强调原子性和可控时机的操作。
立即学习“PHP免费学习笔记(深入)”;
示例:
Db::name('user')->where('id', 123)->update([
'score' => Db::raw('score + 10'),
'updated_at' => time()
]);-
Db::raw('score + 10')不会被转义或拼接,直接参与 SET 字段生成 - 注意字段名大小写:MySQL 默认不区分,但开启
lower_case_table_names=0时,字段名需严格匹配建表定义 - 不能用
Db::raw()做条件判断,比如where('score', '>', Db::raw('limit'))在 TP6.0+ 是支持的,但老版本可能报错,建议统一用whereRaw()
setInc() 和 setDec() 与模型 inc() 的关键区别
setInc() 是 Db 类的静态方法,不依赖模型实例,也不走模型事件和验证,属于“裸 SQL 包装层”。而模型上的 inc() 是实例方法,会触发 before_write 等钩子,也会受自动时间戳、类型转换影响。
性能差异不大,但兼容性很不同:模型 inc() 在 TP5.1 中要求必须先查出数据(否则无主键),而 setInc() 从 TP5.0 就支持纯条件更新。
- 推荐在无需业务逻辑干预时优先用
Db::name('table')->where(...)->setInc('field', 1) -
setInc('field', 0)是合法的,但实际无意义;传负数如-5等价于dec('field', 5),但语义混乱,别这么写 - TP6.0+ 中
setInc()支持多字段,如setInc(['view_count' => 1, 'last_view_time' => time()]),但第二个字段不会被当表达式处理,只是普通赋值
并发下自增不安全?别指望 inc() 自带锁
ThinkPHP 所有自增方法都不自动加数据库行锁。如果两个请求几乎同时执行 inc('stock'),可能出现“读-改-写”竞态,导致少减 1。
这不是 TP 的 bug,是所有 ORM 的共性限制。真实业务中,库存、余额这类字段必须靠数据库原生命令保障原子性。
- 正确做法是用
Db::raw('stock - 1')配合 WHERE 条件做乐观锁:where('stock', '>=', 1),再检查影响行数是否为 1 - 高并发场景下,考虑用 Redis 做预减(
decrby),成功后再落库,避免 DB 成瓶颈 - MySQL 的
UPDATE ... SET stock = stock - 1 WHERE id = 1 AND stock >= 1是唯一可靠方案,TP 的setDec()只负责生成这部分 SQL,WHERE 还得你手动加
很多人卡在“为什么我用了 inc 却还是超卖”,答案就在这里:TP 不解决并发控制,它只负责把你的意图翻译成 SQL —— 而那条 SQL 本身是否安全,得你来设计。


















