Transmitir información sobre quién eliminó el registro a un desencadenador Eliminar


11

Al configurar un seguimiento de auditoría no tengo problemas para rastrear quién está actualizando o insertando registros en una tabla, sin embargo, rastrear quién elimina registros parece más problemático.

Puedo rastrear Inserciones / Actualizaciones incluyendo en Insertar / Actualizar el campo "Actualizado por". Esto permite que el activador INSERT / UPDATE tenga acceso al campo "UpdatedBy" a través de inserted.UpdatedBy. Sin embargo, con el disparador Eliminar no se insertan / actualizan datos. ¿Hay alguna manera de pasar información al desencadenador Eliminar de modo que pueda saber quién eliminó el registro?

Aquí hay un disparador Insertar / Actualizar

ALTER TRIGGER [dbo].[trg_MyTable_InsertUpdate] 
ON [dbo].[MyTable]
FOR INSERT, UPDATE
AS  

INSERT INTO AuditTable (IdOfRecordedAffected, UserWhoMadeChanges) 
VALUES (inserted.ID, inserted.LastUpdatedBy)
FROM inserted 

Usando SQL Server 2012


1
Mira esta respuesta. SUSER_SNAME()es la clave para obtener quién borró el registro.
Kin Shah

1
Gracias Kin, sin embargo, no creo SUSER_SNAME()que funcione en una situación como una aplicación web donde un solo usuario podría ser utilizado para la comunicación de la base de datos para toda la aplicación.
gusano web

1
No mencionó que estaba llamando a una aplicación web.
Kin Shah

Lo siento Kin, debería haber sido más específico para el tipo de aplicación.
gusano web

Respuestas:


10

¿Hay alguna manera de pasar información al desencadenador Eliminar de modo que pueda saber quién eliminó el registro?

Sí: mediante el uso de una llamada muy interesante (y poco utilizada) CONTEXT_INFO. Es esencialmente la memoria de sesión que existe en todos los ámbitos y no está vinculada por las transacciones. Se puede usar para pasar información (cualquier información, bueno, cualquiera que se ajuste al espacio limitado) a disparadores, así como de ida y vuelta entre llamadas subproc / EXEC. Y lo he usado antes para esta misma situación.

Pruebe con lo siguiente para ver cómo funciona. Tenga en cuenta que me estoy convirtiendo CHAR(128)antes de CONVERT(VARBINARY(128), ... Esto es para forzar el relleno en blanco para que sea más fácil volver a convertirlo al VARCHARsacarlo, CONTEXT_INFO()ya que VARBINARY(128)está rellenado con 0x00s.

SELECT CONTEXT_INFO();
-- Initially = NULL

DECLARE @EncodedUser VARBINARY(128);
SET @EncodedUser = CONVERT(VARBINARY(128),
                            CONVERT(CHAR(128), 'I deleted ALL your records! HA HA!')
                          );
SET CONTEXT_INFO @EncodedUser;

SELECT CONTEXT_INFO() AS [RawContextInfo],
       RTRIM(CONVERT(VARCHAR(128), CONTEXT_INFO())) AS [DecodedUser];

Resultados:

0x492064656C6574656420414C4C20796F7572207265636F7264732120484120484121202020202020...
I deleted ALL your records! HA HA!

PONIENDOLO TODO JUNTO:

  1. La aplicación debe llamar a un procedimiento almacenado "Eliminar" que pasa el nombre de usuario (o lo que sea) que está eliminando el registro. Supongo que este ya es el modelo que se está utilizando, ya que parece que ya está rastreando las operaciones de Insertar y Actualizar.

  2. El procedimiento almacenado "Eliminar" hace:

    DECLARE @EncodedUser VARBINARY(128);
    SET @EncodedUser = CONVERT(VARBINARY(128),
                                CONVERT(CHAR(128), @UserName)
                              );
    SET CONTEXT_INFO @EncodedUser;
    
    -- DELETE STUFF HERE
  3. El disparador de auditoría hace:

    -- Set the INT value in LEFT (currently 50) to the max size of [UserWhoMadeChanges]
    INSERT INTO AuditTable (IdOfRecordedAffected, UserWhoMadeChanges) 
       SELECT del.ID, COALESCE(
                         LEFT(RTRIM(CONVERT(VARCHAR(128), CONTEXT_INFO())), 50),
                         '<unknown>')
       FROM DELETED del;
  4. Tenga en cuenta que, como señaló @SeanGallardy en un comentario, debido a otros procedimientos y / o consultas ad hoc que eliminan registros de esta tabla, es posible que:

    • CONTEXT_INFOno se ha configurado y sigue siendo NULL:

      Por esta razón, he actualizado lo anterior INSERT INTO AuditTablepara usar a COALESCEpara predeterminar el valor. O, si no desea un valor predeterminado y requiere un nombre, entonces podría hacer algo similar a:

      DECLARE @UserName VARCHAR(50); -- set to the size of AuditTable.[UserWhoMadeChanges]
      SET @UserName = LEFT(RTRIM(CONVERT(VARCHAR(128), CONTEXT_INFO())), 50);
      
      IF (@UserName IS NULL)
      BEGIN
         ROLLBACK TRAN; -- cancel the DELETE operation
         RAISERROR('Please set UserName via "SET CONTEXT_INFO.." and try again.', 16 ,1);
      END;
      
      -- use @UserName in the INSERT...SELECT
    • CONTEXT_INFOse ha establecido en un valor que no es un nombre de usuario válido y, por lo tanto, puede exceder el tamaño del AuditTable.[UserWhoMadeChanges]campo:

      Por esta razón, agregué una LEFTfunción para garantizar que lo que sea que se extraiga CONTEXT_INFOno rompa el INSERT. Como se indica en el código, solo necesita establecer el 50tamaño real del UserWhoMadeChangescampo.


ACTUALIZACIÓN PARA SQL SERVER 2016 Y MÁS NUEVOS

SQL Server 2016 agregó una versión mejorada de esta memoria por sesión: Contexto de sesión. El nuevo contexto de sesión es esencialmente una tabla hash de pares clave-valor, siendo la "clave" del tipo sysname(es decir NVARCHAR(128)) y el "valor" SQL_VARIANT. Sentido:

  1. Ahora hay una separación de valores, por lo que es menos probable que entre en conflicto con otros usos
  2. Puede almacenar varios tipos, ya no necesita preocuparse por el comportamiento extraño al recuperar el valor a través de CONTEXT_INFO()(para más detalles, consulte mi publicación: ¿Por qué no CONTEXT_INFO () devuelve el valor exacto establecido por SET CONTEXT_INFO? )
  3. Obtiene mucho más espacio: 8000 bytes como máximo por "Valor", hasta 256 kb en total en todas las claves (en comparación con los 128 bytes como máximo CONTEXT_INFO)

Para más detalles, consulte las siguientes páginas de documentación:


El problema con este enfoque es que es MUY volátil. Cualquier sesión puede configurar esto, como tal, puede sobrescribir cualquier elemento configurado previamente. ¿Quieres realmente romper tu aplicación? haga que un solo desarrollador sobrescriba lo que espera. Recomiendo encarecidamente NO usar esto y tener un enfoque estándar que pueda requerir un cambio de arquitectura. De lo contrario, estás jugando con fuego.
Sean Gallardy el

@SeanGallardy ¿Puede proporcionar un ejemplo real de esto? Sesión == @@SPID. Esta es la memoria PER-Session / Connection. Una sesión no puede sobrescribir la información de contexto de otra sesión. Y cuando la sesión cierra la sesión, el valor desaparece. No existe un "elemento previamente establecido".
Solomon Rutzky

1
No dije "otra sesión". Dije que cualquier objeto en el alcance de la sesión puede hacer esto. Entonces, un desarrollador escribe un sproc para guardar su propia información "contextual" y ahora la suya se sobrescribe. Había una aplicación con la que tenía que lidiar que usaba este mismo patrón, lo vi suceder ... era un software de recursos humanos. Permítame decirle lo feliz que era que las personas NO fueran pagadas a tiempo debido a un "error" por parte de uno de los desarrolladores que escribía un nuevo SP que actualizaba erróneamente la información de contexto para la sesión de lo que se "suponía" que era. Solo dando un ejemplo, he sido testigo de por qué no usar este método.
Sean Gallardy

@SeanGallardy Ok, gracias por aclarar ese punto. Pero todavía es solo un punto parcialmente válido. Para que esa situación suceda, ese "otro" proceso debería llamarse dentro de este. O, si está hablando de algún otro proceso que podría eliminarse de esta tabla y comenzar el gatillo, eso es algo que puede probarse. Es una condición de carrera, que es algo a tener en cuenta (al igual que en todas las aplicaciones multiproceso), y no es una razón para no usar esta técnica. Y entonces haré una actualización menor para hacer precisamente eso. Gracias por traer esta posibilidad.
Solomon Rutzky

2
Estoy diciendo que la seguridad como un pensamiento posterior es el problema principal y esta no es la herramienta para resolverlo. Memo estructuras u otros usos que no rompen la aplicación, seguro que no tengo ningún problema. Es absolutamente una razón para NO usarlo. YMMV pero nunca usaría algo tan volátil y desestructurado para algo tan importante como la seguridad. El uso de cualquier tipo de almacenamiento de escritura de usuario compartido para la seguridad es una idea terrible en general. El diseño adecuado eliminaría la necesidad de cosas como esta, en su mayor parte.
Sean Gallardy

5

No puede hacerlo de esa manera, a menos que esté buscando registrar el ID de usuario del servidor SQL en lugar de una aplicación de nivel uno.

Puede hacer una eliminación suave al tener una columna llamada DeletedBy y configurarla según sea necesario, luego su activador de actualización puede hacer la eliminación real (o archivar el registro, generalmente evito las eliminaciones duras cuando sea posible y legal), así como actualizar su seguimiento de auditoría . Para forzar que se realicen eliminaciones de esa manera, defina un on deleteactivador que genere un error. Si no desea agregar una columna a su tabla física, puede definir una vista que agregue la columna y definir instead ofdesencadenantes para manejar la actualización de la tabla base, pero eso puede ser excesivo.


Entiendo tu argumento. De hecho, estaría buscando registrar el usuario de nivel de aplicación.
gusano web

David, en realidad puedes pasar información a los disparadores. Por favor, vea mi respuesta para más detalles :).
Solomon Rutzky

Buena sugerencia aquí, me gusta mucho esta ruta. Mata a dos pájaros al capturar a Who en el mismo paso que desencadenar la eliminación real. Dado que esta columna va a ser NULL para cada registro en esta tabla, ¿parece que sería un buen uso de la SPARSEcolumna de SQL Server ?
Airn5475

2

¿Hay alguna manera de pasar información al desencadenador Eliminar de modo que pueda saber quién eliminó el registro?

Sí, aparentemente hay dos formas ;-). Si hay alguna reserva sobre el uso CONTEXT_INFOcomo he sugerido en mi otra respuesta aquí , solo pensé en otra forma que tenga una separación funcional más limpia de otros códigos / procesos: use una tabla temporal local.

El nombre de la tabla temporal debe incluir el nombre de la tabla que se está eliminando, ya que ayudará a mantenerlo separado de cualquier otro código que pueda ejecutarse en la misma sesión. Algo en la línea de:
#<TableName>DeleteAudit

Una de las ventajas de una tabla temporal local CONTEXT_INFOes que si alguien en otro proceso, que de alguna manera es llamado desde este proceso "Eliminar" en particular, simplemente utiliza incorrectamente el mismo nombre de tabla temporal, el subproceso a) creará un nuevo tabla temporal del nombre solicitado que estará separada de esta tabla temporal inicial (aunque tenga el mismo nombre), y b) cualquier instrucción DML contra la nueva tabla temporal local en el subproceso no afectará ningún dato en el tabla temporal local creada aquí en el proceso padre, por lo tanto, no se sobrescriben los datos. Por supuesto, si una serie de cuestiones de subproceso una sentencia DML en contra de este nombre de tabla temporal sin emitir una primera Crear una tabla de ese mismo nombre, entonces esas instrucciones DML se afectará a los datos de este cuadro. PERO, en este punto nos estamos poniendo realmenteborde-casey aquí, incluso más que con la probabilidad de usos superpuestos de CONTEXT_INFO(sí, sé que ha sucedido, por eso digo "borde-caso" en lugar de "nunca sucederá").

  1. La aplicación debe llamar a un procedimiento almacenado "Eliminar" que pasa el nombre de usuario (o lo que sea) que está eliminando el registro. Supongo que este ya es el modelo que se está utilizando, ya que parece que ya está rastreando las operaciones de Insertar y Actualizar.

  2. El procedimiento almacenado "Eliminar" hace:

    CREATE TABLE #MyTableDeleteAudit (UserName VARCHAR(50));
    INSERT INTO #MyTableDeleteAudit (UserName) VALUES (@UserName);
    
    -- DELETE STUFF HERE
  3. El disparador de auditoría hace:

    -- Set the datatype and length to be the same as the [UserWhoMadeChanges] field
    DECLARE @UserName VARCHAR(50);
    IF (OBJECT_ID(N'tempdb..#TriggerTestDeleteAudit') IS NOT NULL)
    BEGIN
       SELECT @UserName = UserName
       FROM #TriggerTestDeleteAudit;
    END;
    
    -- catch the following conditions: missing table, no rows in table, or empty row
    IF (@UserName IS NULL OR @UserName NOT LIKE '%[a-z]%')
    BEGIN
      /* -- uncomment if undefined UserName == badness
       ROLLBACK TRAN; -- cancel the DELETE operation
       RAISERROR('Please set UserName via #TriggerTestDeleteAudit and try again.', 16 ,1);
       RETURN; -- exit
      */
      /* -- uncomment if undefined UserName gets default value
       SET @UserName = '<unknown>';
      */
    END;
    
    INSERT INTO AuditTable (IdOfRecordedAffected, UserWhoMadeChanges) 
       SELECT del.ID, @UserName
       FROM DELETED del;

    He probado este código en un disparador y funciona como se esperaba.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.