Déterminer l'ancienne clé primaire dans un déclencheur SQL
-
03-07-2019 - |
Question
Je l'ai déjà fait quelque part, j'en suis sûr!
J'ai une table SQL Server 2000 dont j'ai besoin pour consigner les modifications apportées aux champs dans les mises à jour et les insérer dans une deuxième table de journalisation. Voici une version simplifiée de la structure que j'utilise:
MainTable
ID varchar(10) PRIMARY KEY
DESCRIPTION varchar(50)
LogTable
OLDID varchar(10)
NEWID varchar(10)
Pour tout autre domaine, quelque chose comme ceci fonctionnerait très bien:
Select i.DESCRIPTION As New, d.DESCRIPTION As Old
From Inserted i
LEFT JOIN Deleted d On i.ID=d.ID
... Mais évidemment, la jointure échouerait si l'ID était modifié.
Je ne peux pas modifier les tables de la même manière, le seul pouvoir que j’ai dans cette base de données est de créer un déclencheur.
Sinon, y a-t-il quelqu'un qui peut m'apprendre le voyage dans le temps et je vais revenir dans le passé et me demander à l'époque comment j'ai fait cela? Cheers:)
Modifier:
Je pense que je dois clarifier quelques points ici. Ce n'est pas vraiment ma base de données , c'est un système préexistant sur lequel je n'ai presque aucun contrôle, mis à part l'écriture de ce déclencheur.
Ma question est de savoir comment puis-je récupérer l'ancienne clé primaire si cette clé primaire a été modifiée. Je n'ai pas besoin de me faire dire que je ne devrais pas changer la clé primaire, ni rechercher des clés étrangères, etc. Ce n'est pas mon problème:)
La solution
Est-il possible de supposer que les tables INSERTED et DELETED qui vous sont présentées dans un déclencheur ont la garantie d'être dans le même ordre?
Autres conseils
DECLARE @OldKey int, @NewKey int;
SELECT @Oldkey = [ID] FROM DELETED;
SELECT @NewKey = [ID] FROM INSERTED;
Ceci ne fonctionne que si vous avez une seule ligne. Sinon, vous ne disposez pas de "ancre". pour relier les anciennes et les nouvelles lignes. Vérifiez donc dans votre déclencheur pour > 1 dans INSERTED.
Je ne pense pas que ce soit possible. Imaginez si vous avez 4 lignes dans la table:
1 Val1
2 Val2
3 Val3
4 Val4
Publiez maintenant la mise à jour suivante:
UPDATE MainTable SET
ID = CASE ID WHEN 1 THEN 2 WHEN 2 THEN 1 ELSE ID END
Description = CASE ID WHEN 3 THEN 'Val4' WHEN 4 THEN 'Val3' ELSE Description END
Maintenant, comment allez-vous faire la distinction entre ce qui est arrivé aux lignes 1 & amp; 2 et ce qui est arrivé aux rangées 3 & amp; 4. Et plus important encore, pouvez-vous décrire ce qui est différent entre eux? Tout ce qui vous dit quelles colonnes ont été mises à jour ne vous aidera pas.
S'il est possible dans ce cas qu'il y ait une clé supplémentaire sur la table (par exemple, Description est unique) et que vos règles de mise à jour le permettent, vous pouvez écrire le déclencheur pour empêcher les mises à jour simultanées des deux clés, puis vous pouvez utiliser la valeur la clé n'a pas été mise à jour pour corréler les deux tables.
Si vous devez gérer des insertions / mises à jour sur plusieurs lignes et qu'aucune autre clé n'est garantie pour ne pas changer, la seule façon de procéder consiste à utiliser un déclencheur INSTEAD OF. Par exemple, dans le déclencheur, vous pouvez diviser la commande originale insert / update en une commande par ligne, en récupérant chaque ancien ID avant d'insérer / update.
Dans les déclencheurs de SQL Server, vous avez accès à deux tables: supprimées et insérées. Les deux ont déjà été mentionnés. Voici comment ils fonctionnent en fonction de l’action déclenchée par la gâchette:
INSERT OPERATION
- supprimé - non utilisé
- inséré - contient les nouvelles lignes ajoutées à la table
SUPPRIMER L'OPÉRATION
- supprimé - contient les lignes supprimées de la table
- inséré - non utilisé
OPÉRATION DE MISE À JOUR
- supprimé - contient les lignes telles qu'elles existaient avant l'opération UPDATE
- inséré - contient les lignes telles qu'elles existeraient après l'opération UPDATE
Celles-ci fonctionnent à tous égards comme des tables. Par conséquent, il est tout à fait possible d’utiliser une opération basée sur les lignes du type suivant (l’opération n’existe que dans la table d’audit, de même que DateChanged):
INSERT INTO MyAuditTable
(ID, FirstColumn, SecondColumn, ThirdColumn, Operation, DateChanged)
VALUES
SELECT ID, FirstColumn, SecondColumn, ThirdColumn, 'Update-Before', GETDATE()
FROM deleted
UNION ALL
SELECT ID, FirstColumn, SecondColumn, ThirdColumn, 'Update-After', GETDATE()
FROM inserted
---- nouveau ---- Ajoutez une colonne d'identité à la table que l'application ne peut pas modifier. Vous pouvez ensuite utiliser cette nouvelle colonne pour joindre les tables insérées aux tables supprimées dans le déclencheur:
ALTER TABLE YourTableName ADD
PrivateID int NOT NULL IDENTITY (1, 1)
GO
---- vieux ---- Ne jamais mettre à jour / modifier les valeurs clés. Comment pouvez-vous faire cela et réparer toutes vos clés étrangères?
Je ne recommanderais jamais d'utiliser un déclencheur qui ne peut pas gérer un ensemble de lignes.
Si vous devez modifier la clé, insérez une nouvelle ligne avec la nouvelle clé et les valeurs appropriées, utilisez SCOPE_IDENTITY () si tel est votre rôle. Supprimer l'ancienne ligne. Connectez-vous à l'ancienne ligne pour indiquer qu'elle a été remplacée par la clé de la nouvelle ligne. J'espère qu'il n'y a pas de clé étrangère sur la clé modifiée dans votre journal ...
Vous pouvez créer une nouvelle colonne d'identité sur la table MainTable (nommée par exemple, correlationid) et mettre en corrélation les tables insérées et supprimées à l'aide de cette colonne. Cette nouvelle colonne doit être transparente pour le code existant.
INSERT INTO LOG(OLDID, NEWID)
SELECT deleted.id AS OLDID, inserted.id AS NEWID
FROM inserted
INNER JOIN deleted
ON inserted.correlationid = deleted.correlationid
Faites attention, vous pouvez insérer des enregistrements en double dans la table du journal.
Bien sûr, personne ne devrait changer la clé primaire sur la table - mais c’est précisément ce que les déclencheurs sont censés être (en partie), c’est d’empêcher les gens de faire des choses qu’ils ne devraient pas faire. C’est une tâche triviale sous Oracle ou MySQL d’écrire un déclencheur qui intercepte les modifications apportées aux clés primaires et les arrête, mais pas du tout facile dans SQL Server.
Ce que vous aimeriez bien sûr pouvoir faire serait de simplement faire quelque chose comme ceci:
if exists
(
select *
from inserted changed
join deleted old
where changed.rowID = old.rowID
and changed.id != old.id
)
... [roll it all back]
C’est pourquoi les gens recherchent l’équivalent de ROWID dans SQL Server. Eh bien, SQL Server ne l’a pas; vous devez donc proposer une autre approche.
Une version rapide, mais malheureusement non sécurisée, consiste à écrire un déclencheur à la place de update qui cherche à savoir si l'une des lignes insérées possède une clé primaire introuvable dans la table mise à jour ou inversement. Cela attraperait la plupart, mais pas toutes, des erreurs:
if exists
(
select *
from inserted lost
left join updated match
on match.id = lost.id
where match.id is null
union
select *
from deleted new
left join inserted match
on match.id = new.id
where match.id is null
)
-- roll it all back
Mais cela ne capture toujours pas une mise à jour comme ...
update myTable
set id = case
when id = 1 then 2
when id = 2 then 1
else id
end
Maintenant, j'ai essayé de faire l'hypothèse que les tables insérées et supprimées sont ordonnées de manière à ce que le fait de parcourir simultanément les tables insérées et supprimées vous donne des lignes correctement appariées. Et cela semble fonctionner. En fait, vous transformez le déclencheur en équivalent des déclencheurs for-each-row disponibles dans Oracle et obligatoires dans MySQL ... mais j'imagine que les performances seront mauvaises sur les mises à jour massives car il ne s'agit pas d'un comportement natif pour SQL Server. En outre, cela dépend de l’hypothèse que je ne trouve aucun document documenté et que j’ai donc du mal à en dépendre. Mais le code structuré de cette façon, APPEARS, fonctionne correctement sur mon installation de SQL Server 2008 R2. Le script à la fin de cet article met en évidence à la fois le comportement de la solution rapide, mais non bombe, et celui de la deuxième solution, pseudo-Oracle.
Si quelqu'un pouvait m'indiquer un endroit où mon hypothèse est documentée et garantie par Microsoft, je serais très reconnaissant ...
begin try
drop table kpTest;
end try
begin catch
end catch
go
create table kpTest( id int primary key, name nvarchar(10) )
go
begin try
drop trigger kpTest_ioU;
end try
begin catch
end catch
go
create trigger kpTest_ioU on kpTest
instead of update
as
begin
if exists
(
select *
from inserted lost
left join deleted match
on match.id = lost.id
where match.id is null
union
select *
from deleted new
left join inserted match
on match.id = new.id
where match.id is null
)
raisError( 'Changed primary key', 16, 1 )
else
update kpTest
set name = i.name
from kpTest
join inserted i
on i.id = kpTest.id
;
end
go
insert into kpTest( id, name ) values( 0, 'zero' );
insert into kpTest( id, name ) values( 1, 'one' );
insert into kpTest( id, name ) values( 2, 'two' );
insert into kpTest( id, name ) values( 3, 'three' );
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
-- This throws an error, appropriately
update kpTest set id = 5, name = 'FIVE' where id = 1
go
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
-- This allows the change, inappropriately
update kpTest
set id = case
when id = 1 then 2
when id = 2 then 1
else id
end
, name = UPPER( name )
go
select * from kpTest
/*
0 ZERO
1 TWO -- WRONG WRONG WRONG
2 ONE -- WRONG WRONG WRONG
3 THREE
*/
-- Put it back
update kpTest
set id = case
when id = 1 then 2
when id = 2 then 1
else id
end
, name = LOWER( name )
go
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
drop trigger kpTest_ioU
go
create trigger kpTest_ioU on kpTest
instead of update
as
begin
declare newIDs cursor for select id, name from inserted;
declare oldIDs cursor for select id from deleted;
declare @thisOldID int;
declare @thisNewID int;
declare @thisNewName nvarchar(10);
declare @errorFound int;
set @errorFound = 0;
open newIDs;
open oldIDs;
fetch newIDs into @thisNewID, @thisNewName;
fetch oldIDs into @thisOldID;
while @@FETCH_STATUS = 0 and @errorFound = 0
begin
if @thisNewID != @thisOldID
begin
set @errorFound = 1;
close newIDs;
deallocate newIDs;
close oldIDs;
deallocate oldIDs;
raisError( 'Primary key changed', 16, 1 );
end
else
begin
update kpTest
set name = @thisNewName
where id = @thisNewID
;
fetch newIDs into @thisNewID, @thisNewName;
fetch oldIDs into @thisOldID;
end
end;
if @errorFound = 0
begin
close newIDs;
deallocate newIDs;
close oldIDs;
deallocate oldIDs;
end
end
go
-- Succeeds, appropriately
update kpTest
set name = UPPER( name )
go
select * from kpTest;
/*
0 ZERO
1 ONE
2 TWO
3 THREE
*/
-- Succeeds, appropriately
update kpTest
set name = LOWER( name )
go
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
-- Fails, appropriately
update kpTest
set id = case
when id = 1 then 2
when id = 2 then 1
else id
end
go
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
-- Fails, appropriately
update kpTest
set id = id + 1
go
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
-- Succeeds, appropriately
update kpTest
set id = id, name = UPPER( name )
go
select * from kpTest;
/*
0 ZERO
1 ONE
2 TWO
3 THREE
*/
drop table kpTest
go