Передача информации о том, кто удалил запись, на триггер удаления

11

При настройке контрольного журнала у меня нет проблем с отслеживанием того, кто обновляет или вставляет записи в таблицу, однако отслеживание того, кто удаляет записи, кажется более проблематичным.

Я могу отслеживать вставки / обновления, включив в поле «Вставить / обновить» поле «Обновлено». Это позволяет триггеру INSERT / UPDATE иметь доступ к полю «UpdatedBy» через inserted.UpdatedBy. Однако с помощью триггера Удалить данные не вставляются / обновляются. Есть ли способ передать информацию на триггер удаления, чтобы он мог знать, кто удалил запись?

Вот триггер вставки / обновления

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 

Использование SQL Server 2012

мотылек
источник
1
Смотрите этот ответ. SUSER_SNAME()это ключ, чтобы узнать, кто удалил запись.
Кин Шах
1
Спасибо Кин, однако я не думаю, что SUSER_SNAME()будет работать в ситуации, как веб-приложение, где один пользователь может быть использован для связи с базой данных для всего приложения.
веб-червь
1
Вы не упомянули, что вызываете веб-приложение.
Кин Шах
Извините, Кин, я должен был более точно определить тип приложения.
веб-червь

Ответы:

10

Есть ли способ передать информацию на триггер удаления, чтобы он мог знать, кто удалил запись?

Да: с помощью очень крутой (и недостаточно используемой функции) называется CONTEXT_INFO. По сути, это сессионная память, которая существует во всех областях и не связана транзакциями. Он может использоваться для передачи информации (любая информация - ну, любая, которая помещается в ограниченном пространстве) для триггеров, а также назад и вперед между вызовами sub-proc / EXEC. И я использовал это раньше для точно такой же ситуации.

  • Контекстная информация VARBINARY (128)

  • Установить с помощью: SET CONTEXT_INFO

  • Получить через: CONTEXT_INFO ()

Проверьте следующее, чтобы увидеть, как это работает. Обратите внимание, что я обращаюсь CHAR(128)до CONVERT(VARBINARY(128), ... Это сделано для того, чтобы принудительно заполнить пробел, чтобы было легче преобразовать его обратно VARCHARпри извлечении из него, CONTEXT_INFO()так VARBINARY(128)как он дополнен правой клавишей 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];

Результаты:

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

ВМЕСТЕ ВСЕ ВМЕСТЕ:

  1. Приложение должно вызвать хранимую процедуру «Удалить», которая передает имя пользователя (или любое другое), которое удаляет запись. Я предполагаю, что это уже используемая модель, поскольку похоже, что вы уже отслеживаете операции вставки и обновления.

  2. Хранимая процедура «Удалить» выполняет:

    DECLARE @EncodedUser VARBINARY(128);
    SET @EncodedUser = CONVERT(VARBINARY(128),
                                CONVERT(CHAR(128), @UserName)
                              );
    SET CONTEXT_INFO @EncodedUser;
    
    -- DELETE STUFF HERE
  3. Триггер аудита выполняет:

    -- 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. Обратите внимание, что, как отметил @SeanGallardy в комментарии, из-за других процедур и / или специальных запросов, удаляющих записи из этой таблицы, возможно, что либо:

    • CONTEXT_INFOне был установлен и все еще NULL:

      По этой причине я обновил выше, INSERT INTO AuditTableчтобы использовать значение COALESCEпо умолчанию. Или, если вы не хотите использовать значение по умолчанию и требовать имя, вы можете сделать что-то похожее на:

      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_INFOбыло установлено значение, которое не является допустимым именем пользователя и, следовательно, может превышать размер AuditTable.[UserWhoMadeChanges]поля:

      По этой причине я добавил LEFTфункцию, гарантирующую, что все, что было извлечено CONTEXT_INFO, не сломает INSERT. Как отмечено в коде, вам просто нужно установить 50фактический размер UserWhoMadeChangesполя.


ОБНОВЛЕНИЕ ДЛЯ SQL SERVER 2016 И НОВОЕ

SQL Server 2016 добавил улучшенную версию памяти для каждого сеанса: контекст сеанса. Новый контекст сеанса, по сути, представляет собой хэш-таблицу пар ключ-значение с типом «ключ» sysname(т.е. NVARCHAR(128)) и значением «значение» SQL_VARIANT. Смысл:

  1. В настоящее время существует разделение ценностей, поэтому вероятность конфликта с другими видами использования ниже.
  2. Вы можете хранить различные типы, больше не нужно беспокоиться о странном поведении при возврате значения через CONTEXT_INFO()(подробности см. В моем посте: Почему CONTEXT_INFO () не возвращает точное значение, установленное SET CONTEXT_INFO? )
  3. Вы получаете гораздо больше места: максимум 8000 байт на «Значение», до 256 КБ для всех ключей (по сравнению с максимумом 128 байт CONTEXT_INFO)

Для получения дополнительной информации, пожалуйста, смотрите следующие страницы документации:

Соломон Руцкий
источник
Проблема этого подхода в том, что он ОЧЕНЬ изменчив. Любой сеанс может установить это, так как он может перезаписать любой ранее установленный элемент. Хотите действительно сломать ваше приложение? есть один разработчик перезаписать то, что вы ожидаете. Я настоятельно рекомендую НЕ использовать это и использовать стандартный подход, который может потребовать изменения архитектуры. В противном случае вы играете с огнем.
Шон Галларди,
@SeanGallardy Не могли бы вы привести пример этого случая? Сессия == @@SPID. Это память PER-Session / Connection. Один сеанс не может перезаписать контекстную информацию другого сеанса. И когда сеанс выходит из системы, значение исчезает. Не существует такого понятия, как «ранее установленный элемент».
Соломон Руцкий,
1
Я не сказал "другой сеанс", я сказал, что любой объект в области сеанса может сделать это. Итак, один разработчик пишет sproc для хранения своей собственной «контекстной» информации, и теперь ваша перезаписывается. Было приложение, с которым мне приходилось иметь дело, которое использовало тот же шаблон, я наблюдал, как это происходит ... это было программное обеспечение HR. Позвольте мне рассказать вам, как счастливым людям НЕ платили вовремя из-за «ошибки» одного из разработчиков, написавших новый SP, который ошибочно обновил контекстную информацию для сеанса по сравнению с тем, чем он «должен был» быть. Просто приведу пример, который я на самом деле засвидетельствовал, почему бы не использовать этот метод.
Шон Галларди,
@SeanGallardy Хорошо, спасибо за разъяснение этого вопроса. Но это все еще только частично верный момент. Для того, чтобы такая ситуация произошла, этот «другой» процесс должен быть вызван внутри этого. Или, если вы говорите о каком-то другом процессе, который может удалять из этой таблицы и запускать триггер, то это то, что можно проверить. Это условие гонки, которое необходимо учитывать (как и во всех многопоточных приложениях), и не является причиной для того, чтобы не использовать эту технику. И поэтому я сделаю небольшое обновление, чтобы сделать это. Спасибо, что подняли эту возможность.
Соломон Руцкий,
2
Я говорю, что безопасность как запоздалая мысль является главной проблемой, и это не инструмент для ее решения. Мемо-структуры или другие виды использования, которые не нарушают работу приложения, конечно, у меня нет проблем. Это абсолютно повод НЕ использовать его. YMMV, но я бы никогда не использовал что-то столь изменчивое и неструктурированное для чего-то такого важного, как безопасность. Использование любого типа общего хранилища с возможностью записи для пользователя в целом - ужасная идея. Правильный дизайн устраняет необходимость в таких вещах, по большей части.
Шон Галларди,
5

Это невозможно, если только вы не хотите записать идентификатор пользователя сервера SQL, а не один уровень приложения.

Вы можете сделать мягкое удаление, имея столбец с именем DeletedBy и устанавливая его по мере необходимости, тогда ваш триггер обновления может выполнять реальное удаление (или архивировать запись, я обычно избегаю жесткого удаления, где это возможно и разрешено законом), а также обновлять журнал аудита. , Чтобы принудительно выполнить удаление таким образом, определите on deleteтриггер, который вызывает ошибку. Если вы не хотите добавлять столбец в вашу физическую таблицу, вы можете определить представление, которое добавляет столбец и определить instead ofтриггеры для обработки обновления базовой таблицы, но это может быть излишним.

Дэвид Спиллетт
источник
Я понимаю вашу точку зрения. Я действительно хотел бы войти в систему пользователя уровня приложения.
веб-червь
Дэвид, на самом деле ты можешь передавать информацию триггерам. Пожалуйста, смотрите мой ответ для деталей :).
Соломон Руцкий
Хорошее предложение, мне очень нравится этот маршрут. Убивает двух птиц, захватывая Кто на том же этапе, что и запуск реального удаления. Так как этот столбец будет иметь значение NULL для каждой записи в этой таблице, кажется, что это будет хорошее использование SPARSEстолбца SQL Server ?
Airn5475
2

Есть ли способ передать информацию на триггер удаления, чтобы он мог знать, кто удалил запись?

Да, видимо, есть два пути ;-). Если есть какие-либо оговорки в отношении использования, CONTEXT_INFOкак я предложил в моем другом ответе , я просто подумал о другом способе, который имеет более чистое функциональное отделение от другого кода / процессов: использовать локальную временную таблицу.

Имя временной таблицы должно включать имя удаляемой таблицы, поскольку это поможет отделить его от любого другого кода, который может выполняться в том же сеансе. Что-то вроде:
#<TableName>DeleteAudit

Одно из преимуществ использования локальной временной таблицы CONTEXT_INFOзаключается в том, что если кто-то в другом процессе, то есть как-то вызывать этот конкретный процесс «Удалить», просто неправильно использует одно и то же имя временной таблицы, подпроцесс а) создаст новый локальный временная таблица запрошенного имени, которая будет отделена от этой исходной временной таблицы (даже если она имеет то же имя), и б) любые операторы DML для новой локальной временной таблицы в подпроцессе не будут влиять на любые данные в Локальная временная таблица создана здесь в родительском процессе, следовательно, нет перезаписи данных. Конечно, если подпроцесс выдает оператор DML для этого имени временной таблицы, не выдав сначала CREATE TABLE с тем же именем, эти операторы DML будут влиять на данные в этой таблице. НО, на данный момент мы получаем действительнов данном случае, даже в большей степени, чем с вероятностью совпадения вариантов использования CONTEXT_INFO(да, я знаю, что это произошло, поэтому я говорю «крайний случай», а не «это никогда не произойдет»).

  1. Приложение должно вызвать хранимую процедуру «Удалить», которая передает имя пользователя (или любое другое), которое удаляет запись. Я предполагаю, что это уже используемая модель, поскольку похоже, что вы уже отслеживаете операции вставки и обновления.

  2. Хранимая процедура «Удалить» выполняет:

    CREATE TABLE #MyTableDeleteAudit (UserName VARCHAR(50));
    INSERT INTO #MyTableDeleteAudit (UserName) VALUES (@UserName);
    
    -- DELETE STUFF HERE
  3. Триггер аудита выполняет:

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

    Я проверил этот код в триггере, и он работает, как ожидалось.

Соломон Руцкий
источник