PDO是PHP操作数据库最常用的扩展,但很多人在写UPDATE语句时都遇到过这样的困惑:代码没有任何报错,SQL语句复制到命令行里也能执行,可程序一跑,数据就是纹丝不动。这类问题往往不是SQL本身写错了,而是PDO的使用姿势出了岔子。本文把最常见的原因整理出来,并逐一给出排查方法。

一、先确认错误是否被静默吞掉了
PDO默认的错误处理模式是ERRMODE_SILENT,也就是说执行失败时它一个字都不说,你如果不主动去查错误信息,就永远不知道哪里出了问题。这是UPDATE不生效却毫无提示的头号元凶。排查的第一步,就是把错误模式改成异常模式:
<?php
$pdo = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8mb4', 'root', '123456', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 出错直接抛异常
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
改成异常模式后,重新执行一遍UPDATE,如果SQL有语法错误、表名写错、字段不存在,马上就会抛出PDOException,问题立刻浮出水面。很多时候不是查不出来,而是根本没让程序告诉你。
如果暂时不方便改全局配置,也可以在execute之后手动检查错误信息:
$stmt = $pdo->prepare("UPDATE users SET name = ? WHERE id = ?");
$stmt->execute(['Tom', 5]);
// 手动检查错误码,0表示无错误
var_dump($stmt->errorCode());
print_r($stmt->errorInfo());
建议养成习惯:开发环境一律用ERRMODE_EXCEPTION,省去大量瞎猜的时间。
二、占位符与绑定参数不匹配
预处理语句的占位符数量必须和传入的参数数量完全一致,多一个少一个都会导致执行失败。而且在SILENT模式下,这个失败同样是安静的。常见错误写法:
// 错误示例:两个占位符只传了一个值 $sql = "UPDATE users SET name = :name, age = :age WHERE id = :id"; $stmt = $pdo->prepare($sql); $stmt->execute([':name' => 'Tom']); // 缺少 age 和 id,执行失败
正确的做法是把所有参数补齐。另外要注意,命名占位符不能在SQL里重复出现两次——比如想把name同时更新到name和nickname两个字段,写成SET name = :v, nickname = :v是不行的,需要用两个不同名字的占位符,在execute时分别传值。
还有一种隐蔽情况:字段值里包含SQL关键字或者绑定类型不对。比如更新一个整型字段,却用PDO::PARAM_STR绑定,MySQL通常能自动转换问题不大,但在某些严格模式或其他数据库(如PostgreSQL)里就会直接报错。稳妥的写法是使用bindParam并明确类型:
$stmt = $pdo->prepare("UPDATE goods SET stock = stock - :n WHERE id = :id");
$stmt->bindValue(':n', 2, PDO::PARAM_INT);
$stmt->bindValue(':id', 108, PDO::PARAM_INT);
$stmt->execute();
三、事务没提交,等于白干
如果代码里开启了事务,UPDATE执行成功但数据没变,十有八九是忘了commit。在事务模式下,所有的写操作都会先锁定在事务里,直到提交才真正落盘。看这个例子:
$pdo->beginTransaction();
$pdo->exec("UPDATE orders SET status = 2 WHERE id = 100");
// 忘记写 $pdo->commit();
// 脚本结束时事务自动回滚,数据什么都没变
更麻烦的是,如果后续代码抛出异常而脚本直接终止,事务会隐式回滚,你甚至看不到任何提示。规范的做法是用try-catch包裹,保证要么提交要么回滚:
try {
$pdo->beginTransaction();
$pdo->exec("UPDATE orders SET status = 2 WHERE id = 100");
$pdo->exec("UPDATE stock SET num = num - 1 WHERE goods_id = 50");
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
throw $e;
}
另外一个相关坑:长事务持有期间,其他连接查到的是旧数据,容易让人误以为UPDATE没生效,实际只是没提交或者被隔离级别挡住了。排查时可以先确认当前连接是否处于事务中($pdo->inTransaction()),再用另一个连接直连数据库核对。
四、rowCount为零不代表执行失败
很多人用rowCount()判断UPDATE是否成功,看到返回0就以为没生效,其实这里有陷阱。MySQL的默认行为是:如果新值和旧值完全一样,受影响行数记为0,但语句本身是执行成功的。比如把name从Tom改成Tom,rowCount返回0,数据其实没问题。
如果希望值相同也算一行,需要在连接参数里加上 PDO::MYSQL_ATTR_FOUND_ROWS:
$pdo = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8mb4', 'root', '123456', [
PDO::MYSQL_ATTR_FOUND_ROWS => true, // 匹配到的行数而非实际修改的行数
]);
反过来讲,如果rowCount真的是0且数据确实没变,那就要检查WHERE条件了。占位符绑定的id类型如果是字符串,而表字段是int,MySQL会做隐式转换通常没事,但如果传进来的是空字符串或null,WHERE id = ''匹配不到任何行,自然一条都改不了。开发时把最终SQL和参数打印出来核对,是最直接的手段。
五、总结排查清单
遇到PDO UPDATE不生效时,可以按下面的顺序逐项排查:
- 开启异常模式:确认
PDO::ATTR_ERRMODE为ERRMODE_EXCEPTION,让错误无处遁形。 - 核对占位符:占位符个数、命名、参数键名是否一一对应,命名占位符不可重复。
- 检查事务:确认
commit()有被调用,用inTransaction()查看当前状态。 - 核对WHERE条件:打印参数确认不是null或空字符串,条件确实能匹配到行。
- 正确理解rowCount:区分执行失败与值未变化两种情况。
- 确认连的库对不对:开发环境和生产环境的数据库配置混淆,是低级但高频的问题。
把这些点过一遍,绝大多数UPDATE不生效的问题都能定位。核心思路只有一个:别让错误静默发生,让程序把真实情况说出来,排查就成功了一大半。