Passando informações sobre quem excluiu o registro para um gatilho Excluir

11

Ao configurar uma trilha de auditoria, não tenho problemas em rastrear quem está atualizando ou inserindo registros em uma tabela; no entanto, rastrear quem exclui registros parece mais problemático.

Eu posso rastrear Inserções / Atualizações incluindo no campo Inserir / Atualizar o "Atualizado por". Isso permite que o acionador INSERT / UPDATE tenha acesso ao campo "UpdatedBy" via inserted.UpdatedBy. No entanto, com o gatilho Excluir, nenhum dado é inserido / atualizado. Existe uma maneira de passar informações para o gatilho Excluir, de modo que ele possa saber quem excluiu o registro?

Aqui está um gatilho Inserir / Atualizar

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 o SQL Server 2012

webworm
fonte
1
Veja esta resposta. SUSER_SNAME()é a chave para obter quem excluiu o registro.
Kin Shah
1
Obrigado Kin, no entanto, acho que não SUSER_SNAME()funcionaria em uma situação como um aplicativo da Web em que um único usuário pode ser usado para comunicação com o banco de dados de todo o aplicativo.
Webworm
1
Você não mencionou que estava chamando um aplicativo Web.
Kin Shah
Desculpe, Kin, eu deveria ter sido mais específico ao tipo de aplicativo.
Webworm

Respostas:

10

Existe uma maneira de passar informações para o gatilho Excluir, de modo que ele possa saber quem excluiu o registro?

Sim: usando um recurso muito interessante (e subutilizado) chamado CONTEXT_INFO. É essencialmente a memória da sessão que existe em todos os escopos e não é vinculada por transações. Ele pode ser usado para transmitir informações (qualquer informação - bem, qualquer que se encaixe no espaço limitado) para acionadores, bem como para frente e para trás entre chamadas subprocessos / EXEC. E eu o usei antes para exatamente essa mesma situação.

Teste com o seguinte para ver como funciona. Observe que estou convertendo para CHAR(128)antes do CONVERT(VARBINARY(128), ... Isso é forçar preenchimento em branco para facilitar a conversão VARCHARquando retirá-lo, CONTEXT_INFO()pois VARBINARY(128)é preenchido com 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!

COLOCANDO TUDO JUNTO:

  1. O aplicativo deve chamar um procedimento armazenado "Excluir" que transmita o nome de usuário (ou o que for) que está excluindo o registro. Presumo que este já seja o modelo que está sendo usado, pois parece que você já está acompanhando as operações de Inserir e Atualizar.

  2. O procedimento armazenado "Excluir" faz:

    DECLARE @EncodedUser VARBINARY(128);
    SET @EncodedUser = CONVERT(VARBINARY(128),
                                CONVERT(CHAR(128), @UserName)
                              );
    SET CONTEXT_INFO @EncodedUser;
    
    -- DELETE STUFF HERE
    
  3. O gatilho de auditoria faz:

    -- 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. Observe que, como o @SeanGallardy apontou em um comentário, devido a outros procedimentos e / ou consultas ad hoc para excluir registros desta tabela, é possível que:

    • CONTEXT_INFOnão foi definido e ainda é NULL:

      Por esse motivo, atualizei o acima INSERT INTO AuditTablepara usar a COALESCEpara padronizar o valor. Ou, se você não quiser um padrão e exigir um nome, poderá fazer algo semelhante 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_INFOfoi definido como um valor que não é um UserName válido e, portanto, pode exceder o tamanho do AuditTable.[UserWhoMadeChanges]campo:

      Por esse motivo, adicionei uma LEFTfunção para garantir que o que for retirado CONTEXT_INFOnão interrompa o INSERT. Conforme observado no código, você só precisa definir o 50tamanho real do UserWhoMadeChangescampo.


ATUALIZAÇÃO PARA SQL SERVER 2016 E MAIS NOVO

O SQL Server 2016 adicionou uma versão aprimorada dessa memória por sessão: Contexto da Sessão. O novo Contexto da Sessão é essencialmente uma tabela de hash de pares Valor-Chave, sendo o "Chave" do tipo sysname(ie NVARCHAR(128)) e o "Valor" SQL_VARIANT. Significado:

  1. Agora existe uma separação de valores com menor probabilidade de entrar em conflito com outros usos
  2. Você pode armazenar vários tipos, sem precisar se preocupar com o comportamento estranho ao recuperar o valor via CONTEXT_INFO()(para obter detalhes, consulte minha postagem: Por que CONTEXT_INFO () não retorna o valor exato definido por SET CONTEXT_INFO? )
  3. Você obtém muito mais espaço: 8000 bytes no máximo por "Valor", até 256kb no total em todas as chaves (em comparação com o máximo de 128 bytes CONTEXT_INFO)

Para detalhes, consulte as seguintes páginas de documentação:

Solomon Rutzky
fonte
O problema com essa abordagem é que ela é MUITO volátil. Qualquer sessão pode definir isso, pois pode substituir qualquer item definido anteriormente. Deseja realmente quebrar sua aplicação? um único desenvolvedor substituirá o que você espera. Eu recomendo NÃO usar isso e ter uma abordagem padrão que pode exigir uma alteração na arquitetura. Caso contrário, você está brincando com fogo.
Sean Gallardy
@SeanGallardy Você pode fornecer um exemplo real disso? Sessão == @@SPID. Esta é a memória PER-Session / Connection. Uma sessão não pode substituir as informações de contexto de outra sessão. E quando a sessão termina, o valor desaparece. Não existe um "item definido anteriormente".
Solomon Rutzky 14/11
1
Eu não disse "outra sessão". Eu disse que qualquer objeto no escopo da sessão pode fazer isso. Portanto, um desenvolvedor escreve um sproc para armazenar suas próprias informações "contextuais" e agora a sua é substituída. Havia um aplicativo que eu tinha que lidar que usava esse mesmo padrão, eu já vi isso acontecer ... era um software de RH. Deixe-me dizer a você como as pessoas felizes NÃO foram pagas no prazo devido a um "erro" de um dos desenvolvedores que escreveu um novo SP que atualizou erroneamente as informações de contexto da sessão do que deveria "ser". Apenas dando um exemplo, eu realmente testemunhei por que não usar esse método.
23414 Sean Gallardy
@SeanGallardy Ok, obrigado por esclarecer esse ponto. Mas ainda é apenas um ponto parcialmente válido. Para que essa situação aconteça, esse "outro" proc teria que ser chamado dentro deste. Ou, se você estiver falando de algum outro procedimento que possa estar excluindo desta tabela e iniciando o gatilho, é algo que pode ser testado. É uma condição de corrida, que é algo a ser levado em consideração (assim como em todos os aplicativos multithread), e não é um motivo para não usar essa técnica. E, portanto, farei uma pequena atualização para fazer exatamente isso. Obrigado por trazer essa possibilidade.
Solomon Rutzky
2
Estou dizendo que a segurança como uma reflexão posterior é o principal problema e essa não é a ferramenta para resolvê-lo. Estruturas de memorando ou outros usos que não quebram o aplicativo, com certeza não tenho nenhum problema. É absolutamente uma razão para NÃO usá-lo. YMMV, mas eu nunca usaria algo tão volátil e desestruturado para algo que é importante, como segurança. Usar qualquer tipo de armazenamento gravável do usuário compartilhado para segurança é uma péssima idéia em geral. O design adequado eliminaria a necessidade de coisas como essa, na maior parte.
23414 Sean Gallardy
5

Você não pode fazer isso, a menos que esteja procurando gravar o ID do usuário do servidor SQL em vez de um nível de aplicativo.

Você pode fazer uma exclusão reversível tendo uma coluna chamada DeletedBy e definindo-a conforme necessário, para que seu gatilho de atualização faça a exclusão real (ou arquive o registro, eu geralmente evito exclusões físicas sempre que possível e legal), além de atualizar sua trilha de auditoria . Para forçar exclusões a serem feitas dessa maneira, defina um on deletegatilho que gera um erro. Se você não deseja adicionar uma coluna à sua tabela física, pode definir uma exibição que inclua a coluna e defina instead ofgatilhos para manipular a atualização da tabela base, mas isso pode ser um exagero.

David Spillett
fonte
Eu entendo o seu ponto. Na verdade, eu gostaria de registrar o usuário no nível do aplicativo.
Webworm
David, na verdade você pode passar informações para gatilhos. Por favor, veja minha resposta para mais detalhes :).
Solomon Rutzky
Boa sugestão aqui, eu realmente gosto desta rota. Mata dois coelhos capturando Who na mesma etapa que aciona a exclusão real. Como essa coluna será NULL para todos os registros nesta tabela, parece que seria um bom uso da SPARSEcoluna do SQL Server ?
precisa saber é o seguinte
2

Existe uma maneira de passar informações para o gatilho Excluir, de modo que ele possa saber quem excluiu o registro?

Sim, aparentemente existem duas maneiras ;-). Se houver alguma reserva sobre o uso, CONTEXT_INFOcomo sugeri em minha outra resposta aqui , pensei em outra maneira que tenha uma separação funcional mais limpa de outros códigos / processos: use uma tabela temporária local.

O nome da tabela temporária deve incluir o nome da tabela que está sendo excluída, pois isso ajudará a mantê-la separada de qualquer outro código que possa ser executado na mesma sessão. Algo ao longo das linhas de:
#<TableName>DeleteAudit

Um benefício para uma tabela temporária local CONTEXT_INFOé que, se alguém em outro processo - que é de alguma forma chamado desse processo "Excluir" em particular - apenas usa incorretamente o mesmo nome da tabela temporária, o subprocesso a) cria um novo local tabela temporária do nome solicitado que será separado dessa tabela temporária inicial (mesmo que tenha o mesmo nome) eb) quaisquer instruções DML em relação à nova tabela temporária local no subprocesso não afetarão nenhum dado no tabela temporária local criada aqui no processo pai, portanto, nenhuma substituição de dados. Claro que, se um problemas de subprocesso uma declaração DML contra este nome tabela temporária sem antes emitir um CREATE TABLE do mesmo nome, então essas instruções DML irá afetar os dados nesta tabela. MAS, neste momento estamos ficando realmenteaqui, ainda mais do que com a probabilidade de sobreposição de usos CONTEXT_INFO(sim, eu sei que isso aconteceu, é por isso que digo "caso de aresta" em vez de "nunca acontecerá").

  1. O aplicativo deve chamar um procedimento armazenado "Excluir" que transmita o nome de usuário (ou o que for) que está excluindo o registro. Presumo que este já seja o modelo que está sendo usado, pois parece que você já está acompanhando as operações de Inserir e Atualizar.

  2. O procedimento armazenado "Excluir" faz:

    CREATE TABLE #MyTableDeleteAudit (UserName VARCHAR(50));
    INSERT INTO #MyTableDeleteAudit (UserName) VALUES (@UserName);
    
    -- DELETE STUFF HERE
    
  3. O gatilho de auditoria faz:

    -- 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;
    

    Eu testei esse código em um gatilho e funciona como esperado.

Solomon Rutzky
fonte