Pregunta

He hecho esto antes en algún lugar, estoy seguro de ello.

Tengo una tabla de SQL Server 2000 que necesito para registrar los cambios en los campos de las actualizaciones e inserciones en una segunda tabla de registro. A continuación se muestra una versión simplificada de la estructura que estoy usando:

MainTable
ID varchar(10) PRIMARY KEY
DESCRIPTION varchar(50)

LogTable
OLDID varchar(10)
NEWID varchar(10)

Para cualquier otro campo, algo como esto funcionaría muy bien:

Select i.DESCRIPTION As New, d.DESCRIPTION As Old 
From Inserted i
LEFT JOIN Deleted d On i.ID=d.ID

... Pero obviamente la unión fallaría si se cambiara la ID.

No puedo modificar las Tablas de manera que el único poder que tengo en esta base de datos sea crear un disparador.

Alternativamente, ¿hay alguien que pueda enseñarme a viajar en el tiempo y volveré al pasado y me preguntaré cómo hice esto? Saludos :)


Editar:

Creo que necesito aclarar algunas cosas aquí. Esto es en realidad no es mi base de datos , es un sistema preexistente del que casi no tengo control, aparte de escribir este disparador.

Mi pregunta es cómo puedo recuperar la clave principal anterior si se cambió dicha clave principal. No necesito que me digan que no debo cambiar la clave principal o sobre buscar claves externas, etc. Ese no es mi problema :)

¿Fue útil?

Solución

¿Es posible suponer que las tablas INSERTED y DELETED presentadas en un activador tienen la garantía de estar en el mismo orden?

Otros consejos

DECLARE @OldKey int, @NewKey int;

SELECT @Oldkey = [ID] FROM DELETED;
SELECT @NewKey = [ID] FROM INSERTED;

Esto solo funciona si tienes una sola fila. De lo contrario, no tienes " ancla " para enlazar filas antiguas y nuevas. Así que compruebe en su gatillo para > 1 en INSERTADO.

No creo que sea posible. Imagínese si tiene 4 filas en la tabla:

1  Val1
2  Val2
3  Val3
4  Val4

Ahora publique la siguiente actualización:

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

Ahora, ¿cómo vas a distinguir entre lo que sucedió en las filas 1 y amp; 2 y qué pasó con las filas 3 y amp; 4. Y lo que es más importante, ¿puedes describir lo que es diferente entre ellos? Todo lo que te dice qué columnas se han actualizado no te ayudará.

Si es posible en este caso que haya una clave adicional en la tabla (p. ej., Descripción es ÚNICA), y sus reglas de actualización lo permiten, puede escribir el disparador para evitar actualizaciones simultáneas a ambas claves, y luego puede usar cualquiera la clave no se ha actualizado para correlacionar las dos tablas.

Si debe manejar las inserciones / actualizaciones de varias filas, y no hay una clave alternativa que garantice no cambiar, la única forma en que puedo ver esto es usar un disparador INSTEAD OF. Por ejemplo, en el activador, podría dividir el comando original de insertar / actualizar en un comando por fila, capturando cada ID anterior antes de insertar / actualizar.

Dentro de los activadores en SQL Server, tiene acceso a dos tablas: eliminadas e insertadas. Ambos ya han sido mencionados. Así es como funcionan dependiendo de la acción en la que se dispara el activador:

INSERTAR OPERACIÓN

  • eliminado - no utilizado
  • insertado: contiene las nuevas filas que se agregan a la tabla

BORRAR OPERACIÓN

  • eliminado: contiene las filas que se eliminan de la tabla
  • insertado - no utilizado

OPERACIÓN ACTUALIZADA

  • eliminado: contiene las filas tal como existirían antes de la operación ACTUALIZAR
  • insertado: contiene las filas tal como existirían después de la operación UPDATE

Estos funcionan en todos los sentidos como tablas. Por lo tanto, es totalmente posible utilizar una operación basada en filas como la siguiente (la operación solo existe en la tabla de auditoría, al igual 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

---- nuevo ---- agregue una columna de identidad a la tabla que la aplicación no puede cambiar, luego puede usar esa nueva columna para unir las tablas insertadas a las eliminadas dentro del activador:

ALTER TABLE YourTableName ADD
    PrivateID int NOT NULL IDENTITY (1, 1)
GO

---- viejo ---- Nunca actualice / cambie los valores clave. ¿Cómo puedes hacer esto y arreglar todas tus claves externas?

No recomendaría nunca usar un disparador que no pueda manejar un conjunto de filas.

Si debe cambiar la clave, inserte una nueva fila con la clave y los valores nuevos apropiados, use SCOPE_IDENTITY () si eso es lo que está haciendo. Eliminar la fila anterior. Registre la fila anterior que se cambió a la clave de la nueva fila, que ahora debería tener. Espero que no haya ninguna clave externa en la clave modificada en su registro ...

Puede crear una nueva columna de identidad en la tabla de tabla principal (denominada, por ejemplo, correlationid) y correlacionar las tablas insertadas y eliminadas usando esta columna. Esta nueva columna debe ser transparente para el código existente.

INSERT INTO LOG(OLDID, NEWID)
SELECT deleted.id AS OLDID, inserted.id AS NEWID
FROM inserted 
INNER JOIN deleted 
    ON inserted.correlationid = deleted.correlationid

Preste atención, puede insertar registros duplicados en la tabla de registro.

Por supuesto, nadie debería cambiar la clave principal en la mesa, pero eso es exactamente para lo que se supone que son los activadores (en parte), es evitar que las personas hagan cosas que no deberían hacer. Es una tarea trivial en Oracle o MySQL para escribir un disparador que intercepta los cambios en las claves primarias y las detiene, pero no es fácil en SQL Server.

Lo que, por supuesto, te encantaría poder hacer sería simplemente hacer algo como esto:

if exists
  (
  select *
    from inserted changed
           join deleted old
   where changed.rowID = old.rowID
     and changed.id != old.id
  )
... [roll it all back]

Es por eso que la gente sale a buscar en Google el equivalente en SQL Server de ROWID. Bueno, SQL Server no lo tiene; así que tienes que encontrar otro enfoque.

Una versión rápida, pero lamentablemente no a prueba de bombas, es escribir un disparador en lugar de actualizar que busque si alguna de las filas insertadas tiene una clave principal que no se encuentra en la tabla actualizada o viceversa. Esto atraparía la mayoría de los errores, pero no todos:

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

Pero esto todavía no detecta una actualización como ...

update myTable
   set id = case
              when id = 1 then 2 
              when id = 2 then 1
              else id
              end

Ahora, he intentado suponer que las tablas insertadas y eliminadas están ordenadas de tal manera que el desplazamiento de las tablas insertadas y eliminadas de forma simultánea le proporcionará filas correspondientes. Y esto parece que funciona. En efecto, convierte el disparador en el equivalente de los activadores para cada fila disponibles en Oracle y obligatorios en MySQL ... pero me imagino que el rendimiento será malo en las actualizaciones masivas ya que este no es un comportamiento nativo de SQL Server. También depende de la suposición de que no puedo encontrar ningún documento documentado y, por lo tanto, me resisto a depender. Pero el código estructurado de esa manera parece que funciona correctamente en mi instalación de SQL Server 2008 R2. La secuencia de comandos al final de esta publicación destaca tanto el comportamiento de la solución rápida pero no a prueba de bombas como el comportamiento de la segunda solución, pseudo-Oracle.

Si alguien me pudiera indicar un lugar donde mi suposición esté documentada y garantizada por Microsoft, sería un chico muy agradecido ...

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
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top