Как эмулировать триггер ДОПОЛНИТЕЛЬНОГО ввода в T-SQL/SQL Server для объектов super/subtype (Inheritance)?

Это на 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)
);

Я хочу вставить в sub1, чтобы запустить триггер, который фактически вставляет значение в super, и использует super_id, сгенерированный для размещения в 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?

Ответы

Ответ 1

Иногда триггер BEFORE может быть заменен на AFTER one, но это не похоже на ситуацию в вашей ситуации, поскольку вам явно нужно предоставить значение до того, как вставка будет выполнена. Таким образом, для этой цели наиболее близкой функциональностью, по-видимому, является триггер INSTEAD OF, поскольку @marc_s предложил в своем комментарии.

Обратите внимание, однако, что, как указывают названия этих двух типов триггеров, существует принципиальное различие между триггером BEFORE и INSTEAD OF. Хотя в обоих случаях триггер выполняется в то время, когда действие, определенное оператором, вызывающим триггер, не произошло, в случае триггера INSTEAD OF действие никогда не должно происходить вообще. Реальное действие, которое вам нужно сделать, должно выполняться самим триггером. Это очень не похоже на триггерную функциональность BEFORE, где инструкция всегда должна выполняться, если, конечно, вы явно откатываете ее обратно.

Но есть еще одна проблема для решения на самом деле. Как показывает ваш Oracle script, триггер, который нужно преобразовать, использует другую функцию, не поддерживаемую 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;

Вот как это работает:

  • Такое же количество строк, которые вставляются в sub1, сначала добавляется к super. Сгенерированные значения super_id сохраняются во временном хранилище (табличная переменная называется @new_super).

  • Вновь вставленные super_id теперь вставляются в sub1.

На самом деле ничего слишком сложного, но выше будет работать, только если у вас нет других столбцов в sub1, чем те, которые вы указали в своем вопросе. Если есть другие столбцы, вышеуказанный триггер должен быть немного сложнее.

Проблема состоит в том, чтобы назначить новую super_id каждой вставленной строке отдельно. Один из способов реализации сопоставления может быть следующим:

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;

Как вы можете видеть, в @new_user добавлен столбец IDENTIY(1,1), поэтому временно вставленные значения super_id будут дополнительно перечислены начиная с 1. Чтобы обеспечить сопоставление между новым super_id и новыми данными строки, функция ROW_NUMBER используется для перечисления строк INSERTED. В результате каждая строка в наборе INSERTED теперь может быть связана с одним super_id и, таким образом, дополняется полной строкой данных, которая должна быть вставлена ​​в sub1.

Обратите внимание, что порядок, в котором вставлен новый super_id, может не соответствовать порядку, в котором они назначены. Я считал, что это не проблема. Все новые созданные строки super идентичны, кроме идентификаторов. Итак, все, что вам нужно, это просто взять одну новую super_id за новую строку sub1.

Если, однако, логика вставки в super более сложна и по какой-то причине вам нужно точно запомнить, какой новый super_id был создан для новой строки sub, вы, вероятно, захотите рассмотреть метод отображения, обсуждаемый в этом вопросе:

Ответ 2

В то время как предложение Andriy будет хорошо работать для INSERT из небольшого числа записей, полное окончание таблицы будет выполнено в финальном соединении, так как "перечисляемые" и "@new_super" не индексируются, что приводит к низкой производительности для больших вставок.

Это можно решить, указав первичный ключ в таблице @new_super следующим образом:

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

Это приведет к сканированию SQL-оптимизатора через таблицу "enumerated", но с индексированным соединением на @new_super, чтобы получить новый ключ.