MySQL触发器的INSERT由于VALUES中的“SELECT *”导致基数冲突(...)

MySQL trigger's INSERT causing Cardinality violation due to 'SELECT *' in VALUES (...)

提问人:MagicMatt 提问时间:7/8/2022 更新时间:7/8/2022 访问量:94

问:

由于触发器的写入方式,我无法在具有触发器的 MySQL 表上插入新记录。我收到错误:

Cardinality violation: 1241 Operand should contain 1 column(s)

它本质上是一个审核触发器,将新记录复制到包含其他数据的审核表中。我已经成功地证明了该问题特定于嵌套的“SELECT a.*...”在触发器的 Insert 的 VALUES() 部分中,因为当我将 SELECT 替换为“NEW.xyx”列的逐项列表时,它工作正常。示例如下:

表A:

CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(20) NOT NULL,
  `email` varchar(50) NOT NULL,
  `admin` tinyint(1) NOT NULL DEFAULT 0,
  `date_updated` datetime NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

ALTER TABLE `users`
  ADD PRIMARY KEY (`id`);

表B:

CREATE TABLE `users_audit` (
  `action` varchar(8) NOT NULL,
  `revision` int(11) NOT NULL,
  `id` int(11) NOT NULL,
  `username` varchar(20) NOT NULL,
  `email` varchar(50) NOT NULL,
  `admin` tinyint(1) NOT NULL,
  `date_updated` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

触发器(在插入到 TableA 时导致基数冲突):

CREATE TRIGGER users_ins AFTER INSERT ON users
FOR EACH ROW BEGIN
    INSERT INTO users_audit
    VALUES ('insert', 
            (SELECT IFNULL(MAX(a.revision), 0) + 1 FROM users_audit a where a.id = NEW.id), 
            (SELECT b.* FROM users b WHERE b.id = NEW.id LIMIT 1)
           );    
END

问题出在行 上。上面的行(第一个“select”语句)没有问题。我什至添加了“LIMIT 1”以确保只返回 1 行(它应该这样做,因为“id”是“users”的 PK)。我相信发生错误是因为第二个 SELECT 语句返回多个列(由于“SELECT b.*...”),并且它们不能以逗号分隔,以便“VALUES”中的项数与“users_audit”中的列数匹配。(SELECT b.* FROM user_accounts b WHERE b.id = NEW.id LIMIT 1)

我尝试从第二个选择中删除括号,如下所示:

CREATE TRIGGER users_ins AFTER INSERT ON users
FOR EACH ROW BEGIN
    INSERT INTO users_audit
    VALUES ('insert', 
            (SELECT IFNULL(MAX(a.revision), 0) + 1 FROM users_audit a where a.id = NEW.id), 
            SELECT b.* FROM users b WHERE b.id = NEW.id LIMIT 1
           );    
END

但这甚至不能在myPhpAdmin中运行(我收到语法错误)。

真的没有办法做到这一点而不必手动写出每一列吗?作为记录,我已经让以下内容起作用,但对于我要添加审核的所有表,这将需要大量额外的时间(必须手动写出它们的每一列)。

工作触发器,但必须单独列出 NEW 列太冗长:

CREATE TRIGGER users_ins AFTER INSERT ON users
FOR EACH ROW BEGIN
    INSERT INTO users_audit 
    VALUES ('insert', 
            (SELECT IFNULL(MAX(a.revision), 0) + 1 FROM users_audit a where a.id = NEW.id),
            NEW.id, NEW.username, NEW.email, NEW.admin, NEW.date_updated
           );    
END 

我真的很想避免像那样写出列。肯定有某种方法可以做我正在寻找的事情吗?感谢您的帮助!

MySQL Select 触发嵌 基数

评论

0赞 MagicMatt 7/8/2022
如果您谈论的是使用“for update”来锁定行,我相信使用 InnoDB,子查询本身会导致行锁定(因为它位于 INSERT 内部)。我可能是错的,但这就是我所读到的,因此假设的。
0赞 ysth 7/8/2022
你是对的,没关系

答:

0赞 ysth 7/8/2022 #1

一般来说,使用选择 * 和插入而不使用列列表的列顺序是一个非常糟糕的主意,因此我建议只单独列出新列。在本例中,我看到您有两个密切相关的表,并且您可以期望它们在其中保持同步。但是,如果触发器在添加新列时触发,并且它们在一个表中,但尚未在另一个表中,则可能会出现错误。也就是说,您应该能够使用 INSERT...SELECT 而不是 INSERT...值:usersusers_audit

INSERT INTO users_audit
SELECT
    'insert',
    (SELECT IFNULL(MAX(a.revision), 0) + 1 FROM users_audit a where a.id = NEW.id),
    b.*
    FROM users b WHERE b.id = NEW.id

评论

0赞 MagicMatt 7/8/2022
非常感谢。我一整天都在用头撞墙,试图弄清楚为什么它不起作用。刚刚尝试了您的建议,效果很好。我还知道有关列出列名的最佳实践(或不良做法)。这只是一个宠物项目,所以我不太担心事情会不同步,因为我将是唯一一个接触它的人。