T-SQL我的动态记录触发器正在工作 - 请解释为什么使用它不好?

问题描述 投票:2回答:2

我昨天发布了一个关于我的动态触发器的问题,事实证明我的最后一个问题是由于在我的unpivot中包含了太多的字段。话虽这么说,我的创作正在发挥作用! (邪恶的笑)

很多人表达了你的担忧,这可能会对我的数据库的整体健康状况造成不利影响,我很好奇为什么会这样......请在回顾我的想法之后详细说明。

这是一个触发器,可以放在任何服务器上的任何表上(一旦你有必要的东西)。您需要按照下面的模型或根据自己的喜好调整它。 (即您自己的日志表,您自己跟踪谁做出更改的方式等)

您只需要:触发器,日志表(请参阅下面的内容),以及表中跟踪进行更改的用户的字段(在我们的示例中,所有连接都通过一个集中的SQL配置文件,因此这是我们唯一的方法跟踪哪个用户进行了更改(通过在更新时将其作为参数传递))。

tblChangeLog:

  • ID - int - 非空(标识(1,1))
  • 消息 - varchar(max) - NOT NULL
  • TableName - varchar(50) - NOT NULL
  • PrimaryKey - varchar(50) - NOT NULL
  • Activity - varchar(50) - NOT NULL
  • CreatedByUser - varchar(30) - NOT NULL - DEFAULT('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

为了使这更加普遍,我的DBA提出了一个漂亮的脚本和虚拟表,它将这个触发器存储为虚拟表中的一行。该脚本使用游标并查找数据库中的每个表,并为动态创建每个表的触发器。我们也对它进行了测试,它的功能就像一个魅力,而不是在这个时候在这里打扰。

sql-server tsql triggers dynamic-sql
2个回答
2
投票

我注意到的一些事情:

  1. inserteddeleted中可以有多个记录。 Sql Server只会为语句调用一次触发器,即使它会影响很多行。应用程序可能是在极不可能的情况下构建的,但我已经看到这种情况再次引发人们多次触发触发器。
  2. 处理inserteddeleted之间不保留顺序。
  3. 我会使用sq_executesql的参数名称作为LastUpdUser。我知道您不太可能因组织分配的用户名而导致任何问题,但我总是觉得尽可能多地使用参数,这在这里是可能的。 @activity也是如此。
  4. 性能。您在每次更改时都会执行此操作,并且涉及大量字符串处理。 Sql Server中的字符串处理非常昂贵,因此这可能会产生有意义的性能损失。
  5. 为什么?这个能力是already built-in to Sql Server,从Sql Server 2016 sp2开始,它包含在标准版中(不需要为此获得企业版)。

1
投票

被认为是不好用的主要原因是,您要为每个事务添加大量额外写入。当所有这些步骤都不必要时,您正在写#inserted,#delete和#changes。

由于表结构是相当静态的,因此像这样的触发器应该是静态的。以动态方式为整个数据库创建触发器的想法是可以理解的,但是对这些触发器和表的支持应遵循适当的源代码控制。

当然,SQL注入是人们用动态代码提到的。但是,如果您有可以创建SQL注入的列名,那么您可能会遇到更大的问题。

© www.soinside.com 2019 - 2024. All rights reserved.