65.9K
CodeProject 正在变化。 阅读更多。
Home

LINQ 中的自动事件日志记录

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.38/5 (5投票s)

2009年11月30日

CPOL

3分钟阅读

viewsIcon

36938

downloadIcon

13

一个事件日志记录类,以及一个围绕它构建的自动日志记录 LINQ DataContext 后代。

引言

企业应用程序通常有数十甚至数百个用户能够对数据库记录进行破坏性更改。 因此,开发人员通常需要开发一个系统来记录数据库更改,以确保数据可恢复,并且用户对其操作负责。

在本文中,您将找到一个用于 LINQ to SQL 的日志记录解决方案,并将学习如何修改 LINQ DataContext 以自动记录更改。

背景

数据库更改日志记录通常由使用存储过程的自定义系统处理,或者更常见的是通过数据库触发器处理。 但是这两种选择都会消耗开发人员的时间,并且触发器虽然是非常可靠的选择,但会对数据库的性能产生巨大的影响。 因此,我开发了一个库来处理应用程序层中的数据库更改日志记录。 您所需要做的就是包含事件日志记录代码文件,并创建一个继承自您的自定义 LINQ 类的类。

本文假设您知道如何使用 LINQ,并且已经设置了 LINQ to SQL 类。

初始设置

在使用此代码之前,您要做的第一件事是在数据库中创建一个名为 TableHistory 的表

CREATE TABLE [dbo].[TableHistory] (
    [TableHistoryID] [int] IDENTITY NOT NULL ,
    [TableName] [varchar] (50) NOT NULL ,
    [Key1] [varchar] (50) NOT NULL ,
    [Key2] [varchar] (50) NULL ,
    [Key3] [varchar] (50) NULL ,
    [Key4] [varchar] (50) NULL ,
    [Key5] [varchar] (50) NULL ,
    [Key6] [varchar] (50)NULL ,
    [ActionType] [varchar] (50) NULL ,
    [Property] [varchar] (50) NULL ,
    [OldValue] [varchar] (8000) NULL ,
    [NewValue] [varchar] (8000) NULL ,
    [ActionUserName] [varchar] (50) NOT NULL ,
    [ActionDateTime] [datetime] NOT NULL 
)

创建表后,您需要将其添加到您的自定义 LINQ 类(我将其称为 DboDataContext),从而创建 TableHistory 类。 然后,您需要将 History.cs 文件添加到您的项目中。

您还需要将以下代码添加到您的项目中以获取系统日期

public partial class DboDataContext
{

    [Function(Name = "GetDate", IsComposable = true)]
    public DateTime GetSystemDate()
    {
        MethodInfo mi = MethodBase.GetCurrentMethod() as MethodInfo;
        return (DateTime)this.ExecuteMethodCall(this, mi, new object[] { }).ReturnValue;
    }
}

private static Dictionary<type,> _cachedIL = new Dictionary<type,>();

public static T CloneObjectWithIL<t>(T myObject)
{
    Delegate myExec = null;
    if (!_cachedIL.TryGetValue(typeof(T), out myExec))
    {
        // Create ILGenerator
        DynamicMethod dymMethod = new DynamicMethod("DoClone", 
                                      typeof(T), new Type[] { typeof(T) }, true);
        ConstructorInfo cInfo = myObject.GetType().GetConstructor(new Type[] { });

        ILGenerator generator = dymMethod.GetILGenerator();

        LocalBuilder lbf = generator.DeclareLocal(typeof(T));
        //lbf.SetLocalSymInfo("_temp");

        generator.Emit(OpCodes.Newobj, cInfo);
        generator.Emit(OpCodes.Stloc_0);
        foreach (FieldInfo field in myObject.GetType().GetFields(
           System.Reflection.BindingFlags.Instance | 
           System.Reflection.BindingFlags.Public | 
           System.Reflection.BindingFlags.NonPublic))
        {
            // Load the new object on the eval stack... (currently 1 item on eval stack)
            generator.Emit(OpCodes.Ldloc_0);
            // Load initial object (parameter)      (currently 2 items on eval stack)
            generator.Emit(OpCodes.Ldarg_0);
            // Replace value by field value         (still currently 2 items on eval stack)
            generator.Emit(OpCodes.Ldfld, field);
            // Store the value of the top on the eval stack into
            // the object underneath that value on the value stack.
            //  (0 items on eval stack)
            generator.Emit(OpCodes.Stfld, field);
        }

        // Load new constructed obj on eval stack -> 1 item on stack
        generator.Emit(OpCodes.Ldloc_0);
        // Return constructed object.   --> 0 items on stack
        generator.Emit(OpCodes.Ret);

        myExec = dymMethod.CreateDelegate(typeof(Func<t,>));
        _cachedIL.Add(typeof(T), myExec);
    }
    return ((Func<t,>)myExec)(myObject);
}

我从网上(甚至可能从 CodeProject)获得了上述两种方法,但是时间已经太长了,我不记得从哪里得到的了。

History 类说明

History 类通过创建 TableHistory 记录来记录更改,将被修改表的 Primary Key 的值插入到 Key1、Key2、...、Key6 列中(如果任何表中的 Primary Key 超过 6 个值,您需要修改此设置),在 ActionType 列中设置更改的类型(INSERTUPDATEDELETE),如果恰好是更新操作,则设置旧值和新值,以及进行更改的用户的日期和 Windows 身份。

让我们检查一下调用 RecordLinqInsert 方法时会发生什么

public static void RecordLinqInsert(DboDataContext dbo, IIdentity user, object obj)
{
    TableHistory hist = NewHistoryRecord(obj);

    hist.ActionType = "INSERT";
    hist.ActionUserName = user.Name;
    hist.ActionDateTime = dbo.GetSystemDate();

    dbo.TableHistories.InsertOnSubmit(hist);
}

private static TableHistory NewHistoryRecord(object obj)
{
    TableHistory hist = new TableHistory();

    Type type = obj.GetType();
    PropertyInfo[] keys;
    if (historyRecordExceptions.ContainsKey(type))
    {
        keys = historyRecordExceptions[type].ToArray();
    }
    else
    {
        keys = type.GetProperties().Where(o => AttrIsPrimaryKey(o)).ToArray();
    }

    if (keys.Length > KeyMax)
        throw new HistoryException("object has more than " + KeyMax.ToString() + " keys.");
    for (int i = 1; i <= keys.Length; i++)
    {
        typeof(TableHistory)
            .GetProperty("Key" + i.ToString())
            .SetValue(hist, keys[i - 1].GetValue(obj, null).ToString(), null);
    }
    hist.TableName = type.Name;

    return hist;
}

protected static bool AttrIsPrimaryKey(PropertyInfo pi)
{
    var attrs =
        from attr in pi.GetCustomAttributes(typeof(ColumnAttribute), true)
        where ((ColumnAttribute)attr).IsPrimaryKey
        select attr;

    if (attrs != null && attrs.Count() > 0)
        return true;
    else
        return false;
}

RecordLinqInsert 将数据上下文作为输入(它将使用该数据上下文写入数据库)、用户以及要记录的 LINQ 对象(单个对象,例如,如果您使用的是 AdventureWorks,则可以是 Customer 或 Order 对象)。 然后它调用 NewHistoryRecord 方法,该方法将 LINQ to Objects 与 AttrIsPrimaryKey 方法结合使用,以提取所有 Primary Key 属性,设置 TableHistory 对象的 Key1-KeyN 属性,并返回新的 TableHistory 对象。 该代码将在应用程序中调用,如下所示

DboDataContext dbo = new DboDataContext();
Customer cust = new Customer();
dbo.Customers.InsertOnSubmit(cust);
History.RecordLinqInsert(dbo, WindowsIdentity.GetCurrent(), cust);
dbo.SubmitChanges();

更新是通过为每个更改的属性创建一个新的 TableHistory 记录来处理的。 使用 RecordLinqUpdate 函数的过程类似,但它需要修改之前 LINQ 对象的副本

DboDataContext dbo = new DboDataContext();
Customer cust = dbo.Customers.Where(o => o.CustomerID == 1001).Single();
Customer oldCust = CloneObjectWithIL<customer>(cust);
cust.Name = "ASDF";
History.RecordLinqUpdate(dbo, WindowsIdentity.GetCurrent(), oldCust, cust);
dbo.SubmitChanges();

DboTrackedDataContext 类

最后,我们进入真正酷的部分——自动跟踪 DataContext! 我们从 DboDataContext 派生一个类,如下所示,以跟踪使用 DboTrackedDataContext 所做的所有更改。 为此,我们需要添加一个属性,用于历史记录的连接字符串,并且还需要重写 SubmitChanges 方法。

public class DboTrackedDataContext : DboDataContext
{
    private string historyConnectionString;
    public string HistoryConnectionString
    {
        get 
        {
            if (String.IsNullOrEmpty(historyConnectionString))
                return global::System.Configuration.ConfigurationManager.
                   ConnectionStrings["DboConnectionString"].ConnectionString;
            else
                return historyConnectionString;
        }
        set { historyConnectionString = value; }
    }

    public PIPrimaryTrackedDataContext() : base() { }
    public PIPrimaryTrackedDataContext(string connectionString) 
        : base(connectionString) { }
    public PIPrimaryTrackedDataContext(string connectionString, 
                                       MappingSource mappingSource)
        : base(connectionString, mappingSource) { }
    public PIPrimaryTrackedDataContext(IDbConnection connection)
        : base(connection) { }
    public PIPrimaryTrackedDataContext(IDbConnection connection, 
                                       MappingSource mappingSource) 
        : base(connection, mappingSource) { }

    public List<type> ExcludedTypes = new List<type>() { typeof(TableHistory) };



public override void SubmitChanges(System.Data.Linq.ConflictMode failureMode)
{            
    WindowsIdentity identity = WindowsIdentity.GetCurrent();
    PIPrimaryDataContext histDbo = new PIPrimaryDataContext(HistoryConnectionString);

        ChangeSet changeSet = GetChangeSet();
        foreach (var delete in changeSet.Deletes)
        {
            History.RecordLinqDelete(histDbo, identity, delete);
            if (ExcludedTypes.Contains(delete.GetType())) 
                continue;
        }
        foreach (var update in changeSet.Updates)
        {
            if (ExcludedTypes.Contains(update.GetType()))
                continue;
            object table = GetTable(update.GetType());
            Type tableType = table.GetType();
            MethodInfo getOriginal = tableType.GetMethod("GetOriginalEntityState");
            object oldObj = getOriginal.Invoke(table, new object[] { update });
            History.RecordLinqUpdate(histDbo, identity, oldObj, update);
        }

        base.SubmitChanges(failureMode);

        foreach (var insert in changeSet.Inserts)
        {
            if (ExcludedTypes.Contains(insert.GetType()))
                continue;
            History.RecordLinqInsert(histDbo, identity, insert);
        }
        histDbo.SubmitChanges();
    }
}

如您所见,重写的 SubmitChanges 方法调用 GetChangeSet() 来搜索更改列表,并记录每个插入、更新和删除。 然后,它会在最后提交所有这些更改,以便通过仅进行一个额外的数据库事务来提高日志记录效率。

关注点

这是一个有趣的项目,并且结果非常好。 在不到 400 行代码中,您就可以拥有一个功能齐全的自动数据更改跟踪系统,它比传统的触发器方法更有效且更易于维护。 此外,您可以自动提取进行更改的人的 Windows 用户名。

历史

  • 1.0 - 文章发布。
© . All rights reserved.