You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PHP事务中MySQL触发器未触发异常问题求助

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 11:00:56