PAYATHON 2026

MariaDB 中 AFTER UPDATE Trigger 执行失败

支付老李

结论

问题不在 AFTER UPDATE,而是 Trigger 内部再次查询了触发它的 som_accounting,并通过子查询读取了正在更新的 som_ticket。

单独执行这段 SQL 时,这些表不在 Trigger 的调用上下文中,因此语句可以运行。在 Trigger 中,som_accounting 正被外层 UPDATE 使用,此时再次读取该表,会触发 MariaDB 对 Trigger 和存储函数的表访问限制。驱动显示的 unexpected end of stream 只是连接层错误,不一定是服务端返回的真正错误。

这里无须重新查询 som_accounting。当前发票的主键和付款状态都可以直接从 OLD、NEW 中取得,再使用 UPDATE ... JOIN 更新关联票券即可。

推荐实现

DELIMITER //

CREATE TRIGGER somt_au_accounting
AFTER UPDATE ON som_accounting
FOR EACH ROW
BEGIN
    IF OLD.ID_paymentstatus = 1
       AND NEW.ID_paymentstatus = 3 THEN

        UPDATE som_ticket AS t
        INNER JOIN som_accountingticket AS act
            ON act.ID_ticket = t.ticketID
        SET
            t.ID_ticketstatus = 4,
            t.activation_date = CURRENT_DATE()
        WHERE act.ID_accounting = NEW.accountingID
          AND t.ID_ticketstatus = 1;

    END IF;
END//

DELIMITER ;

这段 Trigger 只处理付款状态从 1 变为 3 的情况。它使用 NEW.accountingID 定位本次更新的发票,通过 som_accountingticket 找到关联票券,并将状态为 1 的票券更新为 4。整个过程不再查询 som_accounting,也不需要在子查询中再次读取 som_ticket。

如果更新时允许修改 accountingID,需要按照业务规则决定使用 OLD.accountingID 还是 NEW.accountingID。主键通常不会改变,因此使用 NEW.accountingID 更直观。

为什么原 SQL 在控制台可以运行

控制台中执行的是一条独立的 UPDATE:

UPDATE som_ticket
SET ...
WHERE ticketID IN (...);

在这个执行环境中,som_accounting 只是一张普通的查询表,并不是当前由外层语句修改的表。

Trigger 则由下面的语句触发:

UPDATE som_accounting
SET ID_paymentstatus = 3
WHERE accountingID = 568;

执行期间,原 Trigger 中又包含:

SELECT * FROM som_accounting

也就是说,Trigger 试图再次访问正在触发它的表。MariaDB 对 Trigger 和存储函数有额外的表访问限制,用来避免递归修改、结果不确定以及“正在变化的表”等问题。因此,两段看似相同的 SQL,实际运行环境并不相同。

原语句还通过派生表读取了更新目标 som_ticket:

INNER JOIN (SELECT * FROM som_ticket) AS t2

这种写法常用于绕过“不能同时更新和查询同一张表”的限制,但它可能受到优化器是否合并派生表等行为的影响,在 Trigger 中也没有必要。使用 UPDATE ... JOIN,可以直接根据目标表当前行的条件进行过滤。

如何查看真正的错误

DBeaver 显示的错误:

Could not read resultset: unexpected end of stream

不足以说明具体的 SQL 问题。建议在 MariaDB 命令行中执行触发 Trigger 的 UPDATE,随后立即检查:

SHOW WARNINGS;

还应查看 MariaDB 服务端错误日志。如果连接确实被服务端异常关闭,需要确认服务进程是否发生崩溃或重启。

MariaDB 10.5.8 已经是较早的版本。如果改用简化后的 Trigger 后连接仍然断开,应先在使用相同数据的测试环境中升级到受维护的小版本,再尝试复现并检查服务端日志。不能只根据客户端显示的 unexpected end of stream 判断具体的 SQL 错误。

注意事项

  • 如果 ID_paymentstatus 和 ID_ticketstatus 是数值列,应直接使用 1、3、4,无须写成字符串 '1'、'3'、'4'。
  • AFTER UPDATE Trigger 与原始的 UPDATE som_accounting 处于同一个事务中。票券更新失败时,发票状态更新通常也会回滚。
  • 如果一条语句更新多条发票,Trigger 会针对每一行执行一次。
  • 建议为关联查询建立合适的索引,例如:
CREATE INDEX idx_accountingticket_accounting_ticket
    ON som_accountingticket (ID_accounting, ID_ticket);
  • CREATE DEFINER 必须填写真实且存在的账户。如果没有明确需要,可以省略 DEFINER,由 MariaDB 使用当前账户创建 Trigger。

备注:内容仅供参考。