【问题标题】:Fetch dynamic table name in trigger在触发器中获取动态表名
【发布时间】:2019-11-15 19:35:08
【问题描述】:

tb_tickets

tb_sites_21

我正在创建一个触发器

CREATE DEFINER=`root`@`localhost` 
       TRIGGER `color_changed` 
       AFTER INSERT ON `tb_tickets` 
       FOR EACH ROW UPDATE tb_sites_21 
       SET color_status = NEW.status 
       WHERE site_id = NEW.site_id;

它工作得很好,我只需要 tb_sites_21,我希望从创建新条目的 tb_tickets 的 program_id 中挑选这个 21。

类似这样的:

CREATE DEFINER=`root`@`localhost` 
       TRIGGER `color_changed` 
       AFTER INSERT ON `tb_tickets` 
       FOR EACH ROW UPDATE tb_sites_NEW.program_id 
       SET color_status = NEW.status 
       WHERE site_id = NEW.site_id;

 CREATE DEFINER=root@localhost 
         TRIGGER color_changed AFTER INSERT ON tb_tickets FOR EACH ROW 
         SET @table_name := (SELECT CONCAT("tb_sites_" , program_id) 
         FROM tb_tickets 
         WHERE ticket_id = NEW.ticket_id); 
         UPDATE table_name set 
         color_status = NEW.status WHERE site_id = NEW.site_id

我如何做到这一点?

【问题讨论】:

  • 在 mysql 中你不能,而且 mysql 不允许触发器调用动态 sql。如果你有一个已知的表列表,你可以在更新之前对每个表进行测试。
  • 你能解释一下我们如何在这里使用变量吗? @P.鲑鱼
  • 你们哪一点不清楚?
  • 如果你有一个已知的表列表,你可以在更新之前测试每个表如何实现?@P.Salmon
  • 这看起来是个糟糕的设计。为什么不只使用一个表db_sites 和一列program_id

标签: mysql sql triggers


【解决方案1】:

每个 program_id 的表分区是一个棘手的设计:SELECT * FROM sales + @yymm


您不能在触发器内动态 SQL/prepared 语句,因此唯一可行的选项是使用 IF 语句,但您需要提前知道表名。

CREATE DEFINER=`root`@`localhost` 
       TRIGGER `color_changed` 
       AFTER INSERT
BEGIN
 IF NEW.program_id = 1 THEN
   UPDATE tb_sites_1 
   SET color_status = NEW.status 
   WHERE site_id = NEW.site_id;
 ELSEIF NEW.program_id = 2 THEN
   UPDATE tb_sites_2 
   SET color_status = NEW.status 
   WHERE site_id = NEW.site_id;
 ELSEIF ... THEN
   -- ...
 ELSE
   -- ...
 END
END;

【讨论】:

  • 我们能否使用类似这样的东西来实现 CREATE DEFINER=root@localhost TRIGGER color_changed AFTER INSERT ON tb_tickets FOR EACH ROW SET @table_name := (SELECT CONCAT("tb_sites_" , program_id) FROM tb_tickets WHERE ticket_id = NEW.ticket_id); UPDATE table_name set color_status = NEW.status WHERE site_id = NEW.site_id @Lukasz Szozda
  • @art 不,你不能。它需要动态 SQL。
  • (SQL Server 和 MySQL 之间存在很多差异,我建议忽略此答案开头的链接。)
  • @RickJames 关于每桌...的想法仍然是相同的模式。它不会在 RDBMS 之间改变。
【解决方案2】:

与论坛上的许多问题一样,我将尝试退后一步,发现真正的问题是什么;然后解决它。

明显的问题是您已决定使用TRIGGER,但由于两件事而碰壁:(1) 架构有多个相同的表,以及 (2) 触发器不允许“准备” .

备份一下,真正的问题是根据“program_id”存储一些数据。

因此,如果我们放弃使用触发器 (2),我们可以使用存储过程和/或应用程序代码来完成任务。这与我经常说的一句话相吻合:“触发器(和外键)有局限性;你不一定能用它们做任何你想做的事情。”

但是,我认为 (1) 是更严重的设计缺陷。拥有多个相同的表几乎总是“不正确的模式设计”,它们的区别仅在于表名。从长远来看,在单个表中添加一个额外的列 (program_id) 通常会更容易、更有效。

我了解“单表”方法存在例外情况。如果您认为必须有单独的表格,请详细说明,以便我们进一步讨论。

【讨论】:

  • OP 说这是现有设计,他们无法更改。无论我走到哪里,这都是故事——软件开发人员无权重构数据库设计或应用程序代码。在事务中使用过程或使用两个 DML 语句将需要更改应用程序代码。因此触发,使更新自动发生,无需更改应用程序。
【解决方案3】:

您不能使用变量作为表名。无论您走到哪里,这都是关于 SQL 的事实,即查询以及查询中引用的所有表和列必须在解析查询时修复。在触发器的情况下,这意味着表名必须在您 CREATE TRIGGER 时固定。

另一种方法是使用带有PREPARE and EXECUTE 的动态SQL。这允许您从字符串表达式构建查询,并使 MySQL 在运行时解析和执行它。

它可能看起来像这样:

SELECT CONCAT("tb_sites_" , program_id) INTO @table_name
     FROM tb_tickets WHERE ticket_id = NEW.ticket_id; 
SET @sql = CONCAT('UPDATE ', @table_name, 
    ' SET color_status = ? WHERE site_id = ?');
SET @color_status = NEW.status, @site_id = NEW.site_id;
PREPARE stmt FROM @sql;
EXECUTE stmt USING @color_status, @site_id;
DEALLOCATE PREPARE stmt;

很遗憾,这在触发器中不起作用。

https://dev.mysql.com/doc/refman/8.0/en/sql-syntax-prepared-statements.html 说:

预准备语句的 SQL 语法可以在存储过程中使用,但不能在存储函数或触发器中使用。

P.Salmon 和 Lukasz Szozda 的其他 cmets 和答案一直试图解释这一点,但您似乎没有在听。

有三种选择:

1.对每个案例进行硬编码(Lukasz Szozda 的回答)

我会使用CASE 语句而不是IF/THEN/ELSE IF 块链,但逻辑是相同的。你需要

CREATE DEFINER=`root`@`localhost` 
       TRIGGER `color_changed` 
       AFTER INSERT
BEGIN
 CASE NEW.program_id
 WHEN 1 THEN
   UPDATE tb_sites_1 
   SET color_status = NEW.status 
   WHERE site_id = NEW.site_id;
 WHEN 2 THEN
   UPDATE tb_sites_2 
   SET color_status = NEW.status 
   WHERE site_id = NEW.site_id;
 WHEN 3 THEN
   UPDATE tb_sites_3
   SET color_status = NEW.status 
   WHERE site_id = NEW.site_id;
 ...etc...
 END
END

这有个缺点,如果你有很多表要更新,它会很长,并且每次添加新表时都需要重新定义触发器。

2。使用存储过程而不是触发器(Rick James 的回答)

这不使用触发器。相反,它运行两个语句,一个 INSERT 后跟一个相应站点表的 UPDATE。您可以使用 PREPARE/EXECUTE 语法在存储过程中执行此操作。

CREATE PROCEDURE InsertTicket(
  IN in_ticket_id INT,
  IN in_program_id INT, 
  IN in_color_status VARCHAR(10),
  IN in_site_id INT)
BEGIN
  DECLARE table_name VARCHAR(64);

  -- First insert into the tickets table
  INSERT INTO tb_tickets 
    SET ticket_id = in_ticket_id,
        program_id = in_program_id, 
        color_status = in_color_status,
        site_id = in_site_id;

  -- Second, do a dynamic update into the respective site table
  SET table_name = CONCAT('tb_sites_', in_program_id);
  SET @sql = CONCAT('UPDATE ', table_name, 
    ' SET color_status = ? WHERE site_id = ?');
  SET @color_status = in_color_status, @site_id = in_site_id;
  PREPARE stmt FROM @sql;
  EXECUTE stmt USING @color_status, @site_id;
  DEALLOCATE PREPARE stmt;
END

您还可以使用任何应用程序编码语言执行等效的语句对。您不需要执行存储过程。

您可以使用事务来包装这两个语句,因此它们都同时提交,否则都回滚。

此替代方法将要求您更改应用程序插入工单的方式。因此,您必须更改应用程序代码。如果您无权更改应用程序代码,这不是一个好的选择。

3.将站点表重构为一张表(Paul Spiegel 的评论)

有几个人建议这样做,但您说您无权更改桌子设计。这是不幸的,但很常见。在应用程序中更改表格设计的成本很高,因为根据当前表格设计可能会有很多应用程序代码。所有代码都需要重构以支持对表的更改。

【讨论】:

  • 我可以在这里使用触发器来调用存储过程,而该存储过程又将使用动态表名@Bill Karwin
  • 没有。问题是触发器可能已由准备好的语句本身调用,并且在给定会话中一次只能有一个准备好的语句处于活动状态。深入多少层并不重要。
  • 如果我通过案例陈述是否有任何长度限制来触发字符串(如果我有 100 个案例)@Bill Karwin?
  • 触发器主体存储在LONGTEXT 中,因此它可能比您想写的要长得多。 :-) 限制其实就是创建触发器的SQL语句的长度,受max_allowed_packet的限制。
猜你喜欢
  • 2017-02-11
  • 1970-01-01
  • 1970-01-01
  • 2011-12-16
  • 2012-02-08
  • 2021-09-22
  • 2013-08-05
  • 2022-01-27
  • 1970-01-01
相关资源
最近更新 更多