web-dev-qa-db-fra.com

Comment émuler un déclencheur BEFORE INSERT dans T-SQL / SQL Server pour les entités super / sous-type (héritage)?

C'est sur Azure.

J'ai une entité de supertype et plusieurs entités de sous-type, dont ces dernières doivent obtenir leurs clés étrangères à partir de la clé primaire de l'entité de super type sur chaque insert. Dans Oracle, j'utilise un BEFORE INSERT déclencheur pour accomplir cela. Comment accomplir cela dans 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)
);

Je souhaite un insert dans sub1 pour déclencher un déclencheur qui insère réellement une valeur dans super et utilise le super_id généré pour mettre dans sub1.

Dans Oracle, cela serait accompli comme suit:

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;

Veuillez indiquer comment je simulerais cela dans T-SQL, étant donné que T-SQL n'a pas le BEFORE INSERT aptitude?

18
Matthew Moisen

Parfois, un déclencheur BEFORE peut être remplacé par un déclencheur AFTER, mais cela ne semble pas être le cas dans votre situation, car vous devez clairement fournir une valeur avant l'insertion. Donc, à cet effet, la fonctionnalité la plus proche semble être celle du déclencheur INSTEAD OF, Comme @ marc_s l'a suggéré dans son commentaire.

Notez cependant que, comme le suggèrent les noms de ces deux types de déclencheurs, il existe une différence fondamentale entre un déclencheur BEFORE et un déclencheur INSTEAD OF. Alors que dans les deux cas le déclencheur est exécuté au moment où l'action déterminée par l'instruction qui a été invoquée le déclencheur n'a pas eu lieu, dans le cas du déclencheur INSTEAD OF L'action est n'a jamais supposé avoir lieu du tout. L'action réelle que vous devez effectuer doit être effectuée par le déclencheur lui-même . Ceci est très différent de la fonctionnalité de déclenchement BEFORE, où l'instruction doit toujours être exécutée, sauf, bien sûr, si vous la restaurez explicitement.

Mais il y a un autre problème à résoudre en fait. Comme le révèle votre script Oracle, le déclencheur que vous devez convertir utilise une autre fonctionnalité non prise en charge par SQL Server, qui est celle de FOR EACH ROW. Il n'y a pas non plus de déclencheurs par ligne dans SQL Server, uniquement des déclencheurs par instruction. Cela signifie que vous devez toujours garder à l'esprit que les données insérées sont une ligne définie , pas seulement une seule ligne. Cela ajoute plus de complexité, bien que cela conclura probablement la liste des choses dont vous devez tenir compte.

Donc, c'est vraiment deux choses à résoudre:

  • remplacer la fonctionnalité BEFORE;

  • remplacez la fonctionnalité FOR EACH ROW.

Ma tentative de résoudre ces problèmes est ci-dessous:

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;

Voici comment fonctionne ce qui précède:

  1. Le même nombre de lignes que celui inséré dans sub1 Est d'abord ajouté à super. Les valeurs super_id Générées sont stockées dans un stockage temporaire (une variable de table appelée @new_super).

  2. Les super_id Nouvellement insérés sont maintenant insérés dans sub1.

Rien de trop difficile, mais ce qui précède ne fonctionnera que si vous n'avez pas d'autres colonnes dans sub1 Que celles que vous avez spécifiées dans votre question. S'il y a d'autres colonnes, le déclencheur ci-dessus devra être un peu plus complexe.

Le problème est d'affecter les nouveaux super_id À chaque ligne insérée individuellement. Une façon de mettre en œuvre le mappage pourrait être comme ci-dessous:

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;

Comme vous pouvez le voir, une colonne IDENTIY(1,1) est ajoutée à @new_user, Donc les valeurs super_id Insérées temporairement seront en plus énumérées à partir de 1. Pour fournir le mappage entre le nouveau super_id S et les nouvelles lignes de données, la fonction ROW_NUMBER Est également utilisée pour énumérer les lignes INSERTED. Par conséquent, chaque ligne de l'ensemble INSERTED peut désormais être liée à un seul super_id Et donc complétée par une ligne de données complète à insérer dans sub1.

Notez que l'ordre dans lequel les nouveaux super_id Sont insérés peut ne pas correspondre à l'ordre dans lequel ils sont attribués. J'ai considéré que ce n'était pas un problème. Toutes les nouvelles lignes super générées sont identiques sauf pour les ID. Donc, tout ce dont vous avez besoin ici est de prendre une nouvelle super_id Par nouvelle ligne sub1.

Cependant, si la logique d'insertion dans super est plus complexe et pour une raison quelconque, vous devez vous rappeler précisément quelle nouvelle super_id A été générée pour quelle nouvelle ligne sub, vous 'll voudra probablement considérer la méthode de mappage discutée dans cette question de débordement de pile:

29
Andriy M

Alors que la proposition d'Andriy fonctionnera bien pour les INSERT d'un petit nombre d'enregistrements, des analyses complètes de la table seront effectuées sur la jointure finale car `` énumérées '' et `` @new_super '' ne sont pas indexées, ce qui entraîne de mauvaises performances pour les insertions volumineuses.

Cela peut être résolu en spécifiant une clé primaire sur la table @new_super, comme suit:

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

Cela se traduira par l'optimiseur SQL analysant la table "énumérée" mais effectuant une jointure indexée sur @new_super pour obtenir la nouvelle clé.

1
Mark S. Allen