T-SQL Мой триггер динамического ведения журнала работает - пожалуйста, объясните, почему это плохо для использования? - PullRequest
2 голосов
/ 22 марта 2019

Вчера я опубликовал вопрос о моем динамическом триггере, и выяснилось, что моя последняя проблема была связана с включением одного слишком большого количества полей в мой unpivot. Это, как говорится, мое творение работает! (злой смех)

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

Это триггер, который можно поместить на ЛЮБОЙ СТОЛ на ЛЮБОМ СЕРВЕРЕ (если у вас есть необходимые вещи на месте). Вам нужно либо следовать приведенной ниже модели, либо настроить ее по своему вкусу. (т. е. ваша собственная таблица журналов, ваш собственный способ отслеживания внесенных изменений и т. д.)

Все, что вам нужно: Триггер, таблица журналов (см. Мою ниже) и поле в ваших таблицах, которое отслеживает пользователя, который внес изменение (в нашем случае все соединения проходят через один централизованный SQL профиль, так что это единственный способ, которым мы можем отследить, какой пользователь внес изменение (передав его в качестве параметра во время обновления)).

tblChangeLog:

  • ID - int - NOT NULL (Identitity (1, 1))
  • Сообщение - varchar (max) - НЕ NULL
  • TableName - varchar (50) - НЕ NULL
  • PrimaryKey - varchar (50) - НЕ NULL
  • Активность - varchar (50) - НЕ NULL
  • CreatedByUser - varchar (30) - НЕ NULL - ПО УМОЛЧАНИЮ ('System')
  • CreatedDate - datetime - NOT NULL - DEFAULT (getdate ())

Триггер: (еще не добавил удалить / вставить, может делать это только для обновлений) ...

CREATE TRIGGER (your name here) ON (your table here)

AFTER UPDATE

AS

BEGIN

SET NOCOUNT ON;

DECLARE @tableName  sysname
DECLARE @tableId    INT
DECLARE @activity   VARCHAR(50)
DECLARE @sql        nvarchar(MAX)

-- DETECT AN UPDATE (Records present in both inserted and deleted)
IF EXISTS(SELECT * FROM inserted) AND EXISTS(SELECT * FROM deleted)
    BEGIN

        SET @activity = 'UPDATE'

        -- Gets TableName and TableId
        SELECT @tableName = OBJECT_NAME(parent_object_id)
            , @tableId = parent_object_id
        FROM sys.objects 
        WHERE sys.objects.name = OBJECT_NAME(@@PROCID)

        -- Get the user who made the change
        DECLARE @LastUpdUser VARCHAR(50)
        SELECT @LastUpdUser = LastUpdUser FROM inserted

        -- Stores possible column names
        CREATE TABLE #Columns (
            name varchar(100)
        )

        -- Stores only updated columns
        CREATE TABLE #Changes (
            Id              sql_variant,
            FieldName       sysname,
            FieldValue_OLD  sql_variant,
            FieldValue_NEW  sql_variant,
            DateChanged     datetime DEFAULT (GETDATE()),
            LastUpdUser     varchar(50),
            GroupNumber     int
        )

        -- Gathers names of all possible updated columns (excluding generic)
        INSERT INTO #columns
        SELECT Name
        FROM sys.columns
        WHERE object_id = @tableId
            AND Name NOT IN ('LastUpdUser', 'LastUpdDate', 'CreatedByUser', 'CreatedDate', 'ConcurrencyId')

        -- Builds 2 dynamic strings of columns to use in pivot
        DECLARE @castFields nvarchar(max) -- List of columns being cast to sql_variant
        DECLARE @listOfFields nvarchar(max) -- List of columns for unpivot
        SELECT @castFields = COALESCE(@castFields + ', ', '') + ('CAST(' + QUOTENAME(Name) + ' AS sql_variant) [' + Name + ']') FROM #columns
        SELECT @listOfFields = COALESCE(@listOfFields + ', ', '') + QUOTENAME(Name) FROM #columns WHERE Name <> 'Id'

        -- Inserting deleted/inserted data into temp tables
        SELECT * into #deleted FROM deleted
        SELECT * into #inserted FROM inserted

        SELECT @sql = ';WITH unpvt_deleted AS (
                SELECT Id, FieldName, FieldValue
                FROM
                    (SELECT ' + @castFields + '
                    FROM #deleted) p
                UNPIVOT
                    (FieldValue FOR FieldName IN
                        (' + @listOfFields + ')
                ) AS deleted_unpivot
            ),

            unpvt_inserted AS (
                SELECT Id, FieldName, FieldValue
                FROM
                    (SELECT ' + @castFields + '
                     FROM #inserted) p
                UNPIVOT
                    (FieldValue FOR FieldName IN
                        (' + @listOfFields + ')
                ) AS inserted_unpivot
            )

            INSERT INTO #Changes (Id, FieldName, FieldValue_OLD, FieldValue_NEW, LastUpdUser, GroupNumber)
            SELECT COALESCE(D.Id, I.Id) Id
                , COALESCE(D.FieldName, I.FieldName) FieldName
                , D.FieldValue AS FieldValue_OLD
                , I.FieldValue AS FieldValue_NEW
                , ''' + @LastUpdUser + '''
                , DENSE_RANK() OVER(ORDER BY I.Id) AS GroupNumber
            FROM unpvt_deleted D
                FULL OUTER JOIN unpvt_inserted I ON D.Id = I.Id AND D.FieldName = I.FieldName
            WHERE D.FieldValue <> I.FieldValue

            DECLARE @i INT = 1
            DECLARE @lastGroup INT
            SELECT @lastGroup = MAX(GroupNumber) FROM #Changes

            WHILE @i <= @lastGroup
            BEGIN

                DECLARE @Changes VARCHAR(MAX)
                SELECT @Changes = COALESCE(@Changes + ''; '', '''')
                    + UPPER(CAST(FieldName AS VARCHAR)) + '': ''
                    + '''''''' + CAST(FieldValue_OLD AS VARCHAR) + '''''' to ''
                    + '''''''' + CAST(FieldValue_NEW AS VARCHAR) + ''''''''
                FROM #Changes
                WHERE GroupNumber = @i
                ORDER BY GroupNumber

                INSERT INTO tblChangeLog (Message, TableName, PrimaryKey, Activity, CreatedByUser, CreatedDate)
                SELECT Distinct @Changes, ''' + @tableName + ''', CONVERT(VARCHAR, Id), ''' + @activity + ''', LastUpdUser, DateChanged
                FROM #Changes
                WHERE GroupNumber = @i

                SET @Changes = NULL
                SET @i += 1

            END         

            DROP TABLE #Changes
            DROP TABLE #columns
            DROP TABLE #deleted
            DROP TABLE #inserted'

        exec sp_executesql @sql

    END

END

Чтобы сделать это еще более универсальным, мой администратор баз данных разработал изящный скрипт и фиктивную таблицу, которые будут хранить этот триггер в виде строки в фиктивной таблице. Скрипт использует курсор и находит каждую таблицу в базе данных и создает триггер для каждой таблицы на лету. Мы также проверили это, и оно работает как шарм, не беспокоясь о том, чтобы публиковать это здесь в это время.

Ответы [ 2 ]

2 голосов
/ 22 марта 2019

Некоторые вещи, которые я заметил:

  1. В inserted или deleted может быть несколько записей. Sql Server будет вызывать триггер только один раз для оператора, даже если он влияет на множество строк. Возможно, приложение создано там, где это крайне маловероятно, но я видел, как это возвращалось, чтобы кусать людей в триггерах много раз.
  2. Порядок не сохраняется между обработкой inserted и deleted.
  3. Я бы использовал имя параметра с sq_executesql для LastUpdUser. Я знаю, что у вас вряд ли возникнут какие-либо проблемы, вызванные присвоенным организацией именем пользователя, но я всегда чувствую себя лучше при максимально возможном использовании параметров, и это возможно здесь. То же самое касается @activity.
  4. Производительность. Вы делаете это для каждого отдельного изменения , и это требует значительного количества обработки строк. Обработка строк в Sql Server удивительно дорога, так что это может привести к значительному снижению производительности.
  5. Почему? Эта способность уже встроена в Sql Server , а в Sql Server 2016 sp2 она включена в Standard Edition (для этого не нужно приобретать Enterprise).
1 голос
/ 22 марта 2019

Основная причина, по которой вас считают ненадлежащим, заключается в том, что вы добавляете большое количество дополнительных записей в каждую транзакцию.Вы пишете в #inserted, #deleted и #changes, когда все эти шаги не нужны.

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

Конечно, внедрение SQL - это то, что люди упоминают при динамическомкод.Но если у вас есть имена столбцов, которые будут создавать SQL-инъекцию, у вас могут быть большие проблемы.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...