web-dev-qa-db-fra.com

Le déclencheur "AFTER INSERT" de SQL Server ne voit pas la ligne qui vient d'être insérée

Considérez ce déclencheur:

ALTER TRIGGER myTrigger 
   ON someTable 
   AFTER INSERT
AS BEGIN
  DELETE FROM someTable
         WHERE ISNUMERIC(someField) = 1
END

J'ai une table, un tableau, et j'essaie d'empêcher les gens d'insérer de mauvais disques. Pour les besoins de cette question, un mauvais enregistrement a un champ "someField" qui est tout numérique.

Bien sûr, la bonne façon de faire ceci n'est PAS avec un déclencheur, mais je ne contrôle pas le code source… juste la base de données SQL. Donc, je ne peux pas vraiment empêcher l'insertion de la mauvaise ligne, mais je peux la supprimer tout de suite, ce qui est suffisant pour mes besoins.

Le déclencheur fonctionne, avec un problème ... lorsqu’il se déclenche, il ne semble jamais supprimer le mauvais enregistrement qui vient d’être inséré ... il supprime tous les anciens enregistrements défectueux, mais il ne supprime pas celui qui vient d’être inséré. Donc, il y a souvent un mauvais disque qui ne flotte pas et qui n'est pas effacé jusqu'à ce que quelqu'un d'autre vienne et fasse un autre INSERT.

Est-ce un problème dans ma compréhension des déclencheurs? Les nouvelles lignes insérées ne sont-elles pas encore validées pendant l'exécution du déclencheur?

42
Joel Spolsky

Les déclencheurs ne peuvent pas modifier les données modifiées (Inserted ou Deleted), sinon vous pourriez obtenir une récursion infinie à mesure que les modifications invoquaient à nouveau le déclencheur. Une option serait que le déclencheur annule la transaction.

Edit: La raison en est que la norme SQL est que les lignes insérées et supprimées ne peuvent pas être modifiées par le déclencheur. La raison sous-jacente est que les modifications pourraient provoquer une récursion infinie. Dans le cas général, cette évaluation peut impliquer plusieurs déclencheurs dans une cascade mutuellement récursive. Avoir un système qui décide intelligemment d’autoriser de telles mises à jour est une tâche intolérable sur le plan des calculs, il s’agit essentiellement d’une variante du problème { problème bloquant.

La solution acceptée à ce problème consiste à ne pas autoriser le déclencheur à modifier les données en cours de modification, bien qu'il puisse annuler la transaction. 

create table Foo (
       FooID int
      ,SomeField varchar (10)
)
go

create trigger FooInsert
    on Foo after insert as
    begin
        delete inserted
         where isnumeric (SomeField) = 1
    end
go


Msg 286, Level 16, State 1, Procedure FooInsert, Line 5
The logical tables INSERTED and DELETED cannot be updated.

Quelque chose comme ça va annuler la transaction.

create table Foo (
       FooID int
      ,SomeField varchar (10)
)
go

create trigger FooInsert
    on Foo for insert as
    if exists (
       select 1
         from inserted 
        where isnumeric (SomeField) = 1) begin
              rollback transaction
    end
go

insert Foo values (1, '1')

Msg 3609, Level 16, State 1, Line 1
The transaction ended in the trigger. The batch has been aborted.

Vous pouvez inverser la logique. Au lieu de supprimer une ligne non valide après son insertion, écrivez un déclencheur INSTEAD OF pour insérer seulement si vous vérifiez que la ligne est valide.

CREATE TRIGGER mytrigger ON sometable
INSTEAD OF INSERT
AS BEGIN
  DECLARE @isnum TINYINT;

  SELECT @isnum = ISNUMERIC(somefield) FROM inserted;

  IF (@isnum = 1)
    INSERT INTO sometable SELECT * FROM inserted;
  ELSE
    RAISERROR('somefield must be numeric', 16, 1)
      WITH SETERROR;
END

Si votre application ne veut pas gérer les erreurs (comme le dit Joel, c'est le cas dans son application), alors RAISERROR. Il suffit de faire la gâchette en silence pas de faire un insert qui n’est pas valide.

J'ai exécuté ceci sur SQL Server Express 2005 et cela fonctionne. Notez que INSTEAD OF déclenche ne pas provoque la récursivité si vous insérez dans la même table pour laquelle le déclencheur est défini.

39
Bill Karwin

Voici ma version modifiée du code de Bill:

CREATE TRIGGER mytrigger ON sometable
INSTEAD OF INSERT
AS BEGIN
  INSERT INTO sometable SELECT * FROM inserted WHERE ISNUMERIC(somefield) = 1 FROM inserted;
  INSERT INTO sometableRejects SELECT * FROM inserted WHERE ISNUMERIC(somefield) = 0 FROM inserted;
END

Cela permet à l'insertion de toujours réussir, et tous les faux dossiers sont jetés dans vos sometableRejects où vous pourrez les gérer plus tard. Il est important de faire en sorte que votre table de rejets utilise les champs nvarchar pour tout - pas les ints, tinyints, etc. - car s'ils sont rejetés, c'est parce que les données ne sont pas ce que vous espériez.

Cela résout également le problème d'insertions multiples, ce qui entraînera l'échec du déclencheur de Bill. Si vous insérez dix enregistrements simultanément (comme si vous faisiez un select-insert-into) et qu'un seul d'entre eux est faux, le déclencheur de Bill les aurait tous marqués comme étant mauvais. Cela gère n'importe quel nombre de bons et de mauvais enregistrements.

J'ai utilisé cette astuce sur un projet d'entreposage de données dans lequel l'application d'insertion ne savait pas si la logique métier était bonne, et nous avons plutôt utilisé la logique métier dans les déclencheurs. Vraiment méchant pour la performance, mais si vous ne pouvez pas laisser l'insertion échouer, cela fonctionne.

28
Brent Ozar

Je pense que vous pouvez utiliser la contrainte CHECK - c'est exactement ce pour quoi elle a été inventée.

ALTER TABLE someTable 
ADD CONSTRAINT someField_check CHECK (ISNUMERIC(someField) = 1) ;

Ma réponse précédente (également juste à côté peut être un peu exagérée):

Je pense que la bonne façon consiste à utiliser le déclencheur INSTEAD OF pour empêcher l’insertion de données erronées (plutôt que de les supprimer après le mémoire)

12
Dmitry Khalatov

UPDATE: DELETE d’un déclencheur fonctionne à la fois sur MSSql 7 et MSSql 2008.

Je ne suis pas un gourou relationnel, ni un normé SQL. Cependant - contrairement à la réponse acceptée - MSSQL traite très bien les évaluations de déclencheurs r ecursif et imbriqué . Je ne connais pas d'autres SGBDR.

Les options pertinentes sont 'déclencheurs récursifs' et 'déclencheurs imbriqués' . Les déclencheurs imbriqués sont limités à 32 niveaux et par défaut à 1. Les déclencheurs récursifs sont désactivés par défaut, et on ne parle pas de limite - mais franchement, je ne les ai jamais activés, donc je ne sais pas ce qui se passe avec l'inévitable. débordement de pile. Je soupçonne que MSSQL tuerait simplement votre spid (ou qu'il existe une limite récursive).

Bien sûr, cela montre simplement que la réponse acceptée a la mauvaise raison, mais pas qu'elle est incorrecte. Cependant, avant les déclencheurs INSTEAD OF, je me souviens d’avoir écrit des déclencheurs ON INSERT qui mettraient joyeusement à jour les lignes qui viennent d’être insérées. Tout cela a bien fonctionné et comme prévu.

Un test rapide de suppression de la ligne qui vient d'être insérée fonctionne également:

 CREATE TABLE Test ( Id int IDENTITY(1,1), Column1 varchar(10) )
 GO

 CREATE TRIGGER trTest ON Test 
 FOR INSERT 
 AS
    SET NOCOUNT ON
    DELETE FROM Test WHERE Column1 = 'ABCDEF'
 GO

 INSERT INTO Test (Column1) VALUES ('ABCDEF')
 --SCOPE_IDENTITY() should be the same, but doesn't exist in SQL 7
 PRINT @@IDENTITY --Will print 1. Run it again, and it'll print 2, 3, etc.
 GO

 SELECT * FROM Test --No rows
 GO

Vous avez quelque chose d'autre qui se passe ici.

7
Mark Brackett

Depuis la CREATE TRIGGER documentation:

supprimé et inséré sont des tables logiques (conceptuelles). Elles sont structurellement similaire à la table sur dont le déclencheur est défini, c'est-à-dire, la table sur laquelle l'action de l'utilisateur est tenté, et maintenez les anciennes valeurs ou nouvelles valeurs des lignes qui peuvent être modifié par l'action de l'utilisateur. Pour exemple, pour récupérer toutes les valeurs du fichier table supprimée, utilisez: SELECT * FROM deleted

Cela vous donne au moins un moyen de voir les nouvelles données.

Je ne vois rien dans la documentation qui indique que vous ne verrez pas les données insérées lors de l'interrogation de la table normale ...

4
Jon Skeet

J'ai trouvé cette référence:

create trigger myTrigger
on SomeTable
for insert 
as 
if (select count(*) 
    from SomeTable, inserted 
    where IsNumeric(SomeField) = 1) <> 0
/* Cancel the insert and print a message.*/
  begin
    rollback transaction 
    print "You can't do that!"  
  end  
/* Otherwise, allow it. */
else
  print "Added successfully."

Je ne l'ai pas testée, mais logiquement, il semble qu'il faille dp ce que vous cherchez ... plutôt que de supprimer les données insérées, empêchez l'insertion complètement, vous évitant ainsi d'avoir à annuler l'insertion. Il devrait être plus performant et devrait donc pouvoir supporter plus facilement une charge plus élevée.

Edit: Bien sûr, il y a est le potentiel que si l'insertion se produisait à l'intérieur d'une transaction par ailleurs valide, la transaction wole pouvait être annulée, de sorte que vous deviez prendre en compte ce scénario et déterminer si l'insertion d'une transaction non valide ligne de données constituerait une transaction complètement invalide ...

3
BenAlabaster

MS-SQL a un paramètre pour empêcher le déclenchement récursif de déclencheurs. Ceci est configuré via la procédure sp_configure stockée, dans laquelle vous pouvez activer ou désactiver les déclencheurs récursifs ou imbriqués.

Dans ce cas, il serait possible, si vous désactivez les déclencheurs récursifs, de lier l'enregistrement de la table insérée via la clé primaire et d'apporter des modifications à l'enregistrement.

Dans le cas spécifique de la question, ce n'est pas vraiment un problème, car le résultat est de supprimer l'enregistrement, ce qui ne déclenchera pas le déclenchement de ce déclencheur particulier, mais en général cela pourrait être une approche valide. Nous avons implémenté la concurrence optimiste de cette façon.

Le code de votre déclencheur qui pourrait être utilisé de cette manière serait:

ALTER TRIGGER myTrigger
    ON someTable
    AFTER INSERT
AS BEGIN
DELETE FROM someTable
    INNER JOIN inserted on inserted.primarykey = someTable.primarykey
    WHERE ISNUMERIC(inserted.someField) = 1
END
0
Yishai

Je suis tombé sur cette question en cherchant des détails sur la séquence d'événements au cours d'une déclaration d'insertion et d'un déclencheur. J'ai fini par coder de brefs tests pour confirmer le comportement de SQL 2016 (EXPRESS) - et j'ai pensé qu'il serait approprié de les partager, car cela pourrait aider les autres utilisateurs à rechercher des informations similaires. 

Sur la base de mon test, il est possible de sélectionner des données dans la table "insérée" et de les utiliser pour mettre à jour les données insérées. Et, ce qui est intéressant pour moi, les données insérées ne sont pas visibles par les autres requêtes jusqu'à ce que le déclencheur se termine, moment auquel le résultat final est visible (du moins, au mieux, si je pouvais le tester). Je n'ai pas testé cela pour les déclencheurs récursifs, etc. (je m'attendrais à ce que le déclencheur imbriqué ait une visibilité complète des données insérées dans la table, mais ce n'est qu'une supposition).

Par exemple, en supposant que nous ayons la table "table" avec un champ entier "champ" et le champ de clé primaire "pk" et le code suivant dans notre déclencheur d'insertion:

select @value=field,@pk=pk from inserted
update table set field=@value+1 where pk=@pk
waitfor delay '00:00:15'

Nous insérons une ligne avec la valeur 1 pour "champ", puis la ligne se retrouvera avec la valeur 2. En outre, si j'ouvre une autre fenêtre dans SSMS et essayez: select * from table où pk = @pk

où @pk est la clé primaire que j'ai insérée à l'origine, la requête reste vide jusqu'à l'expiration du délai de 15 secondes et affiche ensuite la valeur mise à jour (champ = 2).

Je m'intéressais aux données visibles par les autres requêtes pendant l'exécution du déclencheur (apparemment pas de nouvelles données). J'ai aussi testé avec une suppression ajoutée:

select @value=field,@pk=pk from inserted
update table set field=@value+1 where pk=@pk
delete from table where pk=@pk
waitfor delay '00:00:15'

Encore une fois, l’insert a pris 15 secondes à exécuter. Une requête exécutée dans une session différente ne montrait aucune nouvelle donnée - pendant ou après l'exécution de l'insert + déclencheur (bien que je m'attende à ce que toute identité s'incrémente même si aucune donnée ne semble être insérée).

0
mszil

Votre "déclencheur" fait quelque chose qu'un "déclencheur" n'est pas censé faire. Vous pouvez simplement avoir votre agent de serveur SQL Server exécuté 

DELETE FROM someTable
WHERE ISNUMERIC(someField) = 1

toutes les 1 seconde environ. Pendant que vous y êtes, pourquoi ne pas écrire un joli petit SP pour empêcher les programmeurs d’insérer des erreurs dans votre tableau. Une des bonnes choses à propos des SP est que les paramètres sont dactylographiés. 

0
LongChalk

Est-il possible que l'INSERT soit valide, mais qu'un UPDATE séparé soit effectué après, il est invalide mais ne déclenche pas le déclencheur?

0
SqlACID

Les techniques décrites ci-dessus décrivent assez bien vos options. Mais que voient les utilisateurs? Je ne peux pas imaginer comment un conflit fondamental comme celui-ci entre vous et le responsable du logiciel ne peut aboutir à une confusion et à un antagonisme avec les utilisateurs.

Je ferais tout ce que je pouvais pour trouver un autre moyen de sortir de l'impasse - car d'autres personnes pourraient facilement voir que tout changement que vous apportez aggrave le problème.

MODIFIER:

Je vais marquer mon premier "undelete" et admettre la publication de ce qui précède lorsque cette question est apparue pour la première fois. Bien sûr, je me suis dégonflé quand j'ai vu que c'était de JOEL SPOLSKY. Mais on dirait qu'il a atterri quelque part près. Vous n'avez pas besoin de vote, mais je vais le noter.

Les déclencheurs IME sont rarement la bonne solution pour des problèmes autres que des contraintes d’intégrité détaillées en dehors du domaine des règles commerciales.

0
dkretz