23

这是在 Azure 上。

我有一个超类型实体和几个子类型实体,后者需要在每次插入时从超类型实体的主键中获取它们的外键。在 Oracle 中,我使用BEFORE INSERT触发器来完成此操作。如何在 SQL Server / T-SQL 中实现这一点?

DDL

CREATE TABLE super (
 super_id int IDENTITY(1,1)
 ,subtype_discriminator char(4) CHECK (subtype_discriminator IN ('SUB1', 'SUB2')
 ,CONSTRAINT super_id_pk PRIMARY KEY (super_id)
);
CREATE TABLE sub1 (
 sub_id int IDENTITY(1,1)
,super_id int NOT NULL
,CONSTRAINT sub_id_pk PRIMARY KEY (sub_id)
,CONSTRAINT sub_super_id_fk FOREIGN KEY (super_id) REFERENCES super (super_id)
);

我希望 insert intosub1触发一个触发器,该触发器实际上将一个值插入super并使用super_id生成的 put into sub1

在 Oracle 中,这将通过以下方式完成:

CREATE TRIGGER sub_trg
    BEFORE INSERT ON sub1
    FOR EACH ROW
DECLARE
    v_super_id int; //Ignore the fact that I could have used super_id_seq.CURRVAL
BEGIN
    INSERT INTO super (super_id, subtype_discriminator) 
        VALUES (super_id_seq.NEXTVAL, 'SUB1') 
        RETURNING super_id INTO v_super_id;
    :NEW.super_id := v_super_id;
END;

请告知我将如何在 T-SQL 中模拟这一点,因为 T-SQL 缺乏这种BEFORE INSERT能力?

4

2 回答 2

33

有时BEFORE可以用一个触发器替换触发器AFTER,但在您的情况下似乎并非如此,因为您显然需要在插入发生之前提供一个值。因此,为此,最接近的功能似乎是INSTEAD OF触发功能,正如@marc_s在他的评论中所建议的那样。

但是请注意,正如这两种触发器类型的名称所暗示的那样,BEFORE触发器和触发器之间存在根本区别INSTEAD OF。虽然在这两种情况下,触发器都是在由调用触发器的语句确定的操作尚未发生时执行的,但在INSTEAD OF触发器的情况下,操作根本不应该发生。你需要做的真正动作必须由触发器本身来完成。这与触发器功能非常不同,在BEFORE触发器功能中,语句总是要执行,当然,除非您显式回滚它。

但实际上还有另一个问题需要解决。正如您的 Oracle 脚本所揭示的,您需要转换的触发器使用了 SQL Server 不支持的另一个特性,即FOR EACH ROW. SQL Server 中也没有逐行触发器,只有逐语句触发器。这意味着您需要始终牢记插入的数据是一个行,而不仅仅是单行。这增加了更多的复杂性,尽管这可能会结束您需要考虑的事项列表。

因此,实际上需要解决两件事:

  • 替换BEFORE功能;

  • 更换FOR EACH ROW功能。

我解决这些问题的尝试如下:

CREATE TRIGGER sub_trg
ON sub1
INSTEAD OF INSERT
AS
BEGIN
  DECLARE @new_super TABLE (
    super_id int
  );
  INSERT INTO super (subtype_discriminator)
  OUTPUT INSERTED.super_id INTO @new_super (super_id)
  SELECT 'SUB1' FROM INSERTED;

  INSERT INTO sub (super_id)
  SELECT super_id FROM @new_super;
END;

这就是上面的工作方式:

  1. 与插入的行数相同的行sub1首先添加到super. 生成的super_id值存储在临时存储器(称为 的表变量@new_super)中。

  2. 新插入super_id的 s 现在插入到sub1.

真的没有什么太难的,但只有当您sub1在问题中指定的列之外没有其他列时,上述方法才会起作用。如果还有其他列,则上述触发器将需要更复杂一些。

问题是将新super_id的 s 分别分配给每个插入的行。实现映射的一种方法可能如下所示:

CREATE TRIGGER sub_trg
ON sub1
INSTEAD OF INSERT
AS
BEGIN
  DECLARE @new_super TABLE (
    rownum   int IDENTITY (1, 1),
    super_id int
  );
  INSERT INTO super (subtype_discriminator)
  OUTPUT INSERTED.super_id INTO @new_super (super_id)
  SELECT 'SUB1' FROM INSERTED;

  WITH enumerated AS (
    SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS rownum
    FROM inserted
  )
  INSERT INTO sub1 (super_id, other columns)
  SELECT n.super_id, i.other columns
  FROM enumerated AS i
  INNER JOIN @new_super AS n
  ON i.rownum = n.rownum;
END;

可以看到,IDENTIY(1,1)添加了一列@new_user,因此临时插入的super_id值将从1开始额外枚举。为了提供新super_ids和新数据行之间的映射,该ROW_NUMBER函数也用于枚INSERTED举行。因此,INSERTED集合中的每一行现在都可以链接到一个单独super_id的行,从而补充到要插入的完整数据行sub1

请注意,super_id插入新 s 的顺序可能与分配它们的顺序不匹配。我认为这没有问题。生成的所有新super行除了 ID 之外都是相同的。所以,你需要的只是super_id每新sub1行取一个新的。

但是,如果插入的逻辑super更复杂,并且由于某种原因您需要准确记住为哪个新行super_id生成了哪个新sub行,那么您可能需要考虑这个 Stack Overflow 问题中讨论的映射方法:

于 2013-03-17T20:42:36.427 回答
2

虽然 Andriy 的提议适用于少量记录的 INSERT,但全表扫描将在最终连接上完成,因为 'enumerated' 和 '@new_super' 都没有索引,导致大型插入的性能很差。

这可以通过在 @new_super 表上指定主键来解决,如下所示:

DECLARE @new_super TABLE (
  row_num INT IDENTITY(1,1) PRIMARY KEY CLUSTERED,
  super_id   int
);

这将导致 SQL 优化器扫描“枚举”表,但在 @new_super 上执行索引连接以获取新键。

于 2017-01-12T18:49:26.253 回答