【问题标题】:How to compare old and new field values in a CLR insert and update trigger如何比较 CLR 插入和更新触发器中的新旧字段值
【发布时间】:2016-02-08 17:28:03
【问题描述】:

我们正在考虑使用一些 SQL CLR 触发器来为某些表填充我们的审计日志,我知道如何在服务器中注册 CLR 程序集和触发器,并且当我知道要查找哪些列以及是否必须查找时一切正常只需在审计日志表中插入一条新记录即可。

现在我们希望它独立于 monitored 表架构,这样我们就不需要在添加或重命名到源表的每个新列时编辑和重新部署触发器,我想在我的审计表中保留一些简单的东西,比如包含更改快照的 n XML 列,比如:

<AuditEntry ObjectName='TableName' ObjectId='1'>
    <Field='Firstname'>
        <OldValue>David</OldValue>
        <NewValue>Davide</NewValue>
    </Field>
    <Field='Email'>
        <OldValue/>
        <NewValue>aaa.b@gmail.com</NewValue>
    </Field>
</AuditEntry>

这个 XML 只是一个例子,我只需要了解如何编写我的触发器 C# 代码,这样它就可以逐个字段比较旧行和新行,获取旧值和新值,然后我就知道如何将其转储到XML 文档。

非常感谢, 大卫。

【问题讨论】:

    标签: c# .net sql-server-2008 triggers sqlclr


    【解决方案1】:

    我做了一些测试,我自己解决了这个问题,这里是为了分享整个故事;-)

    1) .NET SQL CLR 触发器,只有 1 个触发器会监听两个表,唯一的假设是被监视的表有一个名为 Id

    using System.Data.SqlClient;
    using System.Xml;
    using Microsoft.SqlServer.Server;
    
    namespace Axis.CLR.SampleObjects
    {
        using System;
        using System.Data;
        using System.Data.SqlTypes;
        using System.Text;
    
        public partial class AuditTrigger
        {
            public const string GetTableContextStatement =
                "SELECT object_name(resource_associated_entity_id) FROM sys.dm_tran_locks WHERE request_session_id = @@spid and resource_type = 'OBJECT'";
    
            [SqlTrigger(Name = "UserNameAudit", Target = "Users", Event = "FOR INSERT")]
            public static void UserNameAudit()
            {
                SqlTriggerContext triggContext = SqlContext.TriggerContext;
    
                //SqlPipe sqlP = SqlContext.Pipe;
    
                using (SqlConnection conn = new SqlConnection("context connection=true"))
                using (SqlCommand sqlComm = conn.CreateCommand())
                {
                    conn.Open();
    
                    // Gets a reference to the affected table name
                    string tableName = string.Empty;
                    using (SqlCommand cmd = new SqlCommand(GetTableContextStatement, conn))
                    {
                        tableName = cmd.ExecuteScalar().ToString();
                    }
    
                    // STORING INSERT AUDIT
    
                    if (triggContext.TriggerAction == TriggerAction.Insert)
                    {
                        #region handling INSERT action
    
                        sqlComm.CommandText = "SELECT * from INSERTED";
                        var reader = sqlComm.ExecuteReader();
    
                        if (reader.Read())
                        {
                            XmlDocument finalDocument = new XmlDocument();
    
                            XmlNode rootElement = finalDocument.CreateNode(XmlNodeType.Element, tableName, string.Empty);
    
                            XmlAttribute newAttribute = finalDocument.CreateAttribute("Id");
                            newAttribute.Value = reader.GetInt64(reader.GetOrdinal("Id")).ToString();
                            rootElement.Attributes.Append(newAttribute);
    
                            newAttribute = finalDocument.CreateAttribute("Operation");
                            newAttribute.Value = "INSERT";
                            rootElement.Attributes.Append(newAttribute);
    
                            finalDocument.AppendChild(rootElement);
    
                            XmlNode createdElement = finalDocument.CreateNode(XmlNodeType.Element, "Fields", string.Empty);
    
                            for (int i = 0; i < reader.FieldCount; i++)
                            {
                                XmlNode fieldElement = finalDocument.CreateNode(XmlNodeType.Element, reader.GetName(i), string.Empty);
    
                                if (reader.IsDBNull(i))
                                {
                                    fieldElement.InnerText = "NULL";
                                }
                                else
                                {
                                    fieldElement.InnerText = reader.GetValue(i).ToString();
                                }
    
                                createdElement.AppendChild(fieldElement);
                            }
    
                            // Node was added
                            rootElement.AppendChild(createdElement);
    
                            // Adds the Audit
    
                            sqlComm.CommandText = "[dbo].[AddAuditTrail]";
                            sqlComm.CommandType = CommandType.StoredProcedure;
    
                            SqlParameter xmlParamA = new SqlParameter("@ObjectId", SqlDbType.BigInt);
                            xmlParamA.Value = reader.GetInt64(reader.GetOrdinal("Id"));
                            sqlComm.Parameters.Add(xmlParamA);
    
                            reader.Close();
    
                            sqlComm.Parameters.AddWithValue("@ObjectName", tableName);
    
                            SqlParameter xmlParamB = new SqlParameter("@TraceXML", SqlDbType.Xml);
                            xmlParamB.Value = new SqlXml(new XmlTextReader(finalDocument.OuterXml, XmlNodeType.Document, null));
                            sqlComm.Parameters.Add(xmlParamB);
    
                            sqlComm.Parameters.AddWithValue("@AuditType", "INSERT");
    
                            sqlComm.ExecuteNonQuery();
    
                            //sqlP.Send(string.Format("Generated AFTER INSERT XML is: '{0}'", finalDocument.OuterXml));
                        }
    
                        #endregion handling INSERT action
                    }
                    else if (triggContext.TriggerAction == TriggerAction.Update)
                    {
                        #region handling UPDATE action
    
                        DataSet values = new DataSet();
                        SqlDataAdapter adapter = new SqlDataAdapter(sqlComm);
    
                        sqlComm.CommandText = "SELECT * from INSERTED";
                        adapter.Fill(values, "INSERTED");
    
                        sqlComm.CommandText = "SELECT * from DELETED";
                        adapter.Fill(values, "DELETED");
    
                        StringBuilder builder = new StringBuilder();
    
                        builder.Append("<Fields>");
    
                        int recordId = 0;
    
                        for (int i = 0; i < values.Tables["INSERTED"].Columns.Count; i++)
                        {
                            string colName = values.Tables["INSERTED"].Columns[i].ColumnName;
    
                            if (colName.ToLower().Equals("id"))
                            {
                                recordId = Convert.ToInt32(values.Tables["DELETED"].Rows[0][i]);
    
                                builder.AppendFormat("<Id value='{0}' />", recordId);
                            }
    
                            // if both nulls or both the same, no audit needed...
    
                            if (values.Tables["INSERTED"].Rows[0].IsNull(i) && values.Tables["DELETED"].Rows[0].IsNull(i))
                            {
                                continue;
                            }
    
                            if (values.Tables["INSERTED"].Rows[0][i].Equals(values.Tables["DELETED"].Rows[0][i]))
                            {
                                continue;
                            }
    
                            builder.AppendFormat("<{0}>", colName);
    
                            // DUMPING OLD VALUE
                            builder.Append("<OldValue>");
    
                            if (values.Tables["DELETED"].Rows[0].IsNull(i))
                            {
                                builder.Append("NULL");
                            }
                            else
                            {
                                builder.Append(values.Tables["DELETED"].Rows[0][i]);
                            }
    
                            builder.Append("</OldValue>");
    
                            // DUMPING NEW VALUE
                            builder.Append("<NewValue>");
    
                            if (values.Tables["INSERTED"].Rows[0].IsNull(i))
                            {
                                builder.Append("NULL");
                            }
                            else
                            {
                                builder.Append(values.Tables["INSERTED"].Rows[0][i]);
                            }
    
                            builder.Append("</NewValue>");
    
                            builder.AppendFormat("</{0}>", colName);
                        }
    
                        builder.Append("</Fields>");
    
                        builder.Insert(0, string.Format("<{0} Id='{1}' Operation='{2}'>", tableName, recordId, "UPDATE"));
                        builder.AppendFormat("</{0}>", tableName);
    
                        // Adds the Audit
    
                        sqlComm.CommandText = "[dbo].[AddAuditTrail]";
                        sqlComm.CommandType = CommandType.StoredProcedure;
    
                        SqlParameter xmlParamA = new SqlParameter("@ObjectId", SqlDbType.BigInt);
                        xmlParamA.Value = recordId;
                        sqlComm.Parameters.Add(xmlParamA);
    
                        sqlComm.Parameters.AddWithValue("@ObjectName", tableName);
    
                        SqlParameter xmlParamB = new SqlParameter("@TraceXML", SqlDbType.Xml);
                        xmlParamB.Value = new SqlXml(new XmlTextReader(builder.ToString(), XmlNodeType.Document, null));
                        sqlComm.Parameters.Add(xmlParamB);
    
                        sqlComm.Parameters.AddWithValue("@AuditType", "UPDATE");
    
                        sqlComm.ExecuteNonQuery();
    
                        //sqlP.Send(string.Format("Generated AFTER UPDATE XML is: '{0}'", builder.ToString()));
    
                        #endregion handling UPDATE action
                    }
                }
            }
        }
    }
    

    2)这里是我用来在sql server上注册触发器并将其链接到两个不同的表(Users和Products)的SQL代码,只有1个CLR触发器但是在SQL server中使用 CLR 将两个触发器创建为外部触发器,每个触发器用于每个表

    USE [Axis_Davide]
    GO
    
    BEGIN TRANSACTION SCRIPT
    ---------------------------------
    
    IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'trAuditTriggerA') AND type in (N'TA'))
    BEGIN
        DROP TRIGGER [dbo].[trAuditTriggerA]
            PRINT('Trigger A was removed');
    END
    
    IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'trAuditTriggerB') AND type in (N'TA'))
    BEGIN
        DROP TRIGGER [dbo].[trAuditTriggerB]
            PRINT('Trigger B was removed');
    END
    
    
    IF EXISTS (SELECT * FROM sys.assemblies asms WHERE asms.name = N'Axis.CLR.SampleObjects' and is_user_defined = 1)
    BEGIN
        DROP ASSEMBLY [Axis.CLR.SampleObjects]
        PRINT('Assembly was removed');
    END
    
    CREATE ASSEMBLY [Axis.CLR.SampleObjects]
    AUTHORIZATION [dbo]
    FROM 'C:\Axis\SQLCLR_Samples\Axis.CLR.SampleObjects.dll'
    WITH PERMISSION_SET = SAFE
    PRINT('Assembly was created');
    
    EXEC('CREATE TRIGGER trAuditTriggerA ON [dbo].[Users] AFTER INSERT, UPDATE AS EXTERNAL NAME [Axis.CLR.SampleObjects].[Axis.CLR.SampleObjects.AuditTrigger].[UserNameAudit]')
    PRINT('Trigger A was created');
    
    EXEC('CREATE TRIGGER trAuditTriggerB ON [dbo].[Products] AFTER INSERT, UPDATE AS EXTERNAL NAME [Axis.CLR.SampleObjects].[Axis.CLR.SampleObjects.AuditTrigger].[UserNameAudit]')
    PRINT('Trigger B was created');
    
    ---------------------------------
    COMMIT TRANSACTION SCRIPT
    

    3)这里是我的审计表的创建语句

    CREATE TABLE [dbo].[AuditTrail]
    (
        [Id] [bigint] IDENTITY(1,1) NOT NULL,
        [AuditDate] [datetime2](7) NOT NULL,
        [UserName] [nvarchar](64) NOT NULL,
        [ObjectId] [bigint] NOT NULL,
        [ObjectName] [nvarchar](128) NOT NULL,
        [TraceXML] [xml] NOT NULL,
        [TraceSize] [int] NOT NULL,
        [AuditType] [nvarchar](16) NOT NULL,
     CONSTRAINT [PK_AuditTrail] PRIMARY KEY CLUSTERED ( [Id] ASC )
    )
    GO
    
    ALTER TABLE [dbo].[AuditTrail] ADD  CONSTRAINT [DF_AuditTrail_AuditDate]  DEFAULT (sysutcdatetime()) FOR [AuditDate]
    GO
    
    ALTER TABLE [dbo].[AuditTrail] ADD  CONSTRAINT [DF_AuditTrail_UserName]  DEFAULT (suser_sname()) FOR [UserName]
    GO
    

    4)这里是触发器调用的存储过程,用于在每次插入/更新时添加新的审计记录

    CREATE PROCEDURE [dbo].[AddAuditTrail]
        @ObjectId bigint, 
        @ObjectName nvarchar(128),
        @TraceXML xml,
        @AuditType nvarchar(16)
    AS
    BEGIN
        -- SET NOCOUNT ON added to prevent extra result sets from
        -- interfering with SELECT statements.
        SET NOCOUNT ON;
    
        INSERT INTO [dbo].[AuditTrail] ([ObjectId], [ObjectName], [TraceXML], [TraceSize], [AuditType])
            VALUES (@ObjectId, @ObjectName, @TraceXML, DATALENGTH(@TraceXML), @AuditType)
    END
    GO
    

    5) 我的审计表的内容如下所示,INSERTUPDATE

    <Users Id="51" Operation="INSERT">
      <Fields>
        <UserName>Davide</UserName>
        <Pass>Test</Pass>
        <Id>51</Id>
        <Email>NULL</Email>
      </Fields>
    </Users>
    
    <Users Id="51" Operation="UPDATE">
      <Fields>
        <Id value="51" />
        <Email>
          <OldValue>NULL</OldValue>
          <NewValue>@</NewValue>
        </Email>
      </Fields>
    </Users>
    

    【讨论】:

      猜你喜欢
      • 2019-08-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-03-09
      • 2014-02-05
      • 2010-12-22
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多