PHP事务中MySQL触发器未触发异常问题求助
当通过PHP脚本启动事务更新AcquisitionItem表时,用于禁止qtt列出现零值的触发器未抛出预期异常,导致零值被成功写入;但在MySQL终端或PHPMyAdmin中执行相同操作时,触发器正常工作,能阻止零值写入并抛出错误。
数据库结构与触发器代码
CREATE TABLE Acquisition ( id int(11) PRIMARY KEY AUTO_INCREMENT, -- 其他字段 ); CREATE TABLE AcquisitionItem ( id int(11) unique NOT NULL AUTO_INCREMENT, acquisition int(11) NOT NULL, qtt decimal(12,3) unsigned NOT NULL, -- 其他字段 CONSTRAINT fkAcquisitionItem FOREIGN KEY (acquisition) REFERENCES Acquisition (id) ); delimiter // CREATE TRIGGER chk_zero_qtt BEFORE UPDATE ON AcquisitionItem FOR EACH ROW BEGIN if new.qtt = 0 then begin signal sqlstate '45000' set message_text = 'Zero at qtt'; end; end if; end // delimiter ;
PHP相关代码
ConnectionFactory类
class ConnectionFactory { // 其他属性 private $conn; function __construct(){ try { $this->conn = new PDO("mysql:host=$this->server;dbname=$this->db", $this->user, $this->pass); $this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch(PDOException $ex) { // 异常处理逻辑 } } public function getConnection(){ return $this->conn; } }
AcquisitionDAO的update方法
class AcquisitionDAO { // 其他方法 public function update(Acquisition $acq): ?string { try { $conn = (new ConnectionFactory())->getConnection(); $itemDAO = new AcquisitionItemDAO(); $dbItems = $itemDAO->selectIdsByAcquisition($acq->getId()); $insertedItems = $this->checkInsertedItems($acq->getItems()); $updatedItems = $this->checkUpdatedItems($dbItems, $acq->getItems()); $deletedItems = $this->checkDeletedItems($dbItems, $acq->getItems()); $conn->beginTransaction(); // 更新Acquisition主表 $stmt = $conn->prepare("update Acquisition set (...) where id = :id"); // 绑定其他参数 $stmt->bindValue(':id', $acq->getId(), PDO::PARAM_INT); $stmt->execute(); // 删除旧项 if(sizeof($deletedItems) > 0){ foreach ($deletedItems as $item) { $itemDAO->delete($conn, $item); } } // 更新现有项 if(sizeof($updatedItems) > 0){ foreach ($updatedItems as $item) { $itemDAO->update($conn, $acq->getId(), $item); } } // 插入新项 if(sizeof($insertedItems) > 0){ foreach ($insertedItems as $item) { $itemDAO->insert($conn, $acq->getId(), $item); } } $con->commit(); // 注意:此处变量名疑似笔误,应为$conn $stmt->closeCursor(); $conn = null; return null; }catch(PDOException $ex){ $conn->rollBack(); // 异常处理逻辑 return $ex->getMessage(); } } }
AcquisitionItemDAO的update方法
class AcquisitionItemDAO { // 其他方法 public function update(PDO $conn, int $AcqId, AcquisitionItem $item): void { $stmt = $conn->prepare("update AcquisitionItem set (...) qtt = :qtt where id = :id"); $stmt->bindValue(':qtt', $item->getQtt(), PDO::PARAM_STR); $stmt->bindValue(':id', $item->getId(), PDO::PARAM_INT); $stmt->execute(); $stmt->closeCursor(); } }
环境信息
MySQL 5.7.40、PHP 7.2.24、Apache 2.4.29,表引擎为InnoDB。
已尝试方案
- 创建存储过程验证DBMS内部事务中触发器正常工作;
- 将触发器从BEFORE改为AFTER;
- 在触发器条件中使用
CAST(new.qtt AS DECIMAL(12,3)) = 0.000替代直接比较; - 移除
$stmt->closeCursor(); - 调整PDO的
ATTR_AUTOCOMMIT、ATTR_EMULATE_PREPARES、ATTR_PERSISTENT属性值。
以上方案均未解决问题,零值仍能被写入。
排查与解决步骤
1. 修正事务提交的变量名笔误
观察AcquisitionDAO的update方法末尾,提交事务时使用了$con->commit(),但之前获取的连接对象是$conn,这会导致未定义变量错误,甚至可能实际未提交当前事务(若$con是其他无效连接)。将代码修正为:
$conn->commit();
这是最可能的直接原因,笔误会导致事务逻辑异常,甚至掩盖触发器抛出的错误。
2. 验证触发器是否被触发
在触发器中添加日志记录,确认PHP执行UPDATE时触发器是否实际运行:
-- 先创建日志表 CREATE TABLE TriggerLog ( id INT AUTO_INCREMENT PRIMARY KEY, event_time DATETIME DEFAULT CURRENT_TIMESTAMP, qtt_value DECIMAL(12,3), message TEXT ); -- 修改触发器 delimiter // CREATE TRIGGER chk_zero_qtt BEFORE UPDATE ON AcquisitionItem FOR EACH ROW BEGIN INSERT INTO TriggerLog(qtt_value, message) VALUES(new.qtt, 'Trigger executed'); IF new.qtt <= 0.000 THEN INSERT INTO TriggerLog(qtt_value, message) VALUES(new.qtt, 'Zero value detected'); SIGNAL SQLSTATE '45000' SET message_text = 'Zero at qtt'; END IF; END // delimiter ;
执行PHP的更新操作后,查询TriggerLog表:
- 若无记录:说明触发器未被触发,需检查PHP执行的UPDATE语句是否命中目标行;
- 有记录但无"Zero value detected":说明
new.qtt的值并非严格等于0,需检查PHP传入的数值精度; - 有"Zero value detected"记录但PHP未捕获异常:说明PDO未正确捕获MySQL的错误信号。
3. 调整PDO参数绑定类型
AcquisitionItemDAO中绑定qtt时使用了PDO::PARAM_STR,但qtt是DECIMAL类型,建议改为不指定类型(让PDO自动推断),或使用PDO::PARAM_FLOAT:
// 方案1:不指定类型,自动推断 $stmt->bindValue(':qtt', $item->getQtt()); // 方案2:使用数值类型绑定 $stmt->bindValue(':qtt', $item->getQtt(), PDO::PARAM_FLOAT);
字符串类型的绑定可能导致MySQL对数值的解析与触发器中的比较逻辑不一致。
4. 强制检查PDO执行错误
确认在事务执行过程中,PDO的错误模式未被意外修改。可以在AcquisitionItemDAO的update方法中手动检查执行结果:
$stmt->execute(); $errorInfo = $stmt->errorInfo(); if ($errorInfo[0] !== '00000') { throw new PDOException($errorInfo[2], (int)$errorInfo[1]); }
强制抛出PDO错误,避免因错误模式被覆盖导致异常丢失。
5. 查看MySQL日志
开启MySQL的通用查询日志(general log)或错误日志,查看PHP发送的SQL语句细节,以及触发器执行时的错误信息:
- 在MySQL配置文件中设置
general_log = 1,重启服务后执行PHP操作,查看日志文件中是否有触发器抛出的错误; - 若日志中存在
Zero at qtt的错误,但PHP未捕获,需检查PDO的连接配置是否正确处理了MySQL的错误信号。
内容的提问来源于stack exchange,提问作者Alexandre

