1

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

现在我们希望它独立于受监控的表模式,这样我们就不需要在添加或重命名到源表的每个新列上编辑和重新部署触发器,我想在我的审计表中坚持一些简单的事情像 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 文档中。

非常感谢,戴维德。

4

1 回答 1

2

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

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 代码我用来在 sql server 上注册触发器并将其链接到两个不同的表(用户和产品),只有 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)我的审计表的内容如下所示,对于一个INSERT和对于一个UPDATE

<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>
于 2013-06-20T17:36:55.100 回答