Entity Framework 快照历史记录

4
我正在尝试使用Code First Entity Framework,并保留特定表的快照历史记录。这意味着对于我想要跟踪的每个表,我都希望有一个重复的表后缀为“_History”。每次更改跟踪表行时,在将新数据保存到原始表之前,从数据库中复制数据到历史表,同时增加一个版本号列。
所以假设我有一个名为Record的表。我有一行(ID:1,Name:One,Version:1)。当我把它改为(ID1:Name:Changed,Version:2)时,Record_History表会增加一行(ID:1,Name:One,Version:1)。
我已经看过很好的例子并知道有使用Entity Framework来保持更改审计日志的库,但我需要每个修订版的实体的完整快照以供SQL报告使用。
在我的C#代码中,我有一个基类,所有我的“Tracked”表对应的实体类都继承自该基类:
public abstract class TrackedEntity
{
    [Column(TypeName = "varchar")]
    [MaxLength(48)]
    [Required]
    public string ModifiedBy { get; set; }

    [Required]
    public DateTime Modified { get; set; }

    public int Version { get; set; }
}

我的一个实体类的示例是:

public sealed class Record : TrackedEntity
{
    [Key]
    public int RecordID { get; set; }

    [MaxLength(64)]
    public string Name { get; set; }
}

现在让我们来看看我遇到的难题。我不想为每个实体单独创建和维护一个_History类。我想要做一些智能的事情,告诉我的DbContext类它所拥有的每个DbSet都应该有一个与TrackedEntity继承类型相关的历史对应表,并且无论何时保存该类型的实体,都将原始值从数据库复制到历史表中。
因此,在我的DbContext类中,我有一个记录的DbSet(以及其他实体的更多DbSets)。
public DbSet<Record> Records { get; set; }

我已经重写了OnModelCreating方法,以便注入新的_History表的映射。但是我无法想出如何使用反射将每个实体的类型传递给DbModelBuilder。

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        //map a history table for each tracked Entity type
        PropertyInfo[] properties = GetType().GetProperties();
        foreach (PropertyInfo property in properties.Where(p => p.PropertyType.IsGenericType 
            && p.PropertyType.Name.StartsWith("DbSet") 
            && p.PropertyType.GetGenericArguments().Length > 0
            && p.PropertyType.GetGenericArguments()[0].IsSubclassOf(typeof(TrackedEntity))))
        {
            Type type = property.PropertyType.GetGenericArguments()[0];
            modelBuilder.Entity<type>().Map(m => //code breaks here, I cannot use the type variable it expects a hard coded Type
            {
                m.ToTable(type.Name + "_History");
                m.MapInheritedProperties();
            });
        }
    }

我甚至不确定像这样使用modelBuilder是否会生成新的History表。我也不知道如何处理保存,我不确定实体映射是否意味着更改会同时保存在两个表中?我可以在我的DbContext中创建一个SaveChanges方法,可以循环遍历我的实体,但我不知道如何使实体保存到第二个表中。

    public int SaveChanges(string username)
    {
        //duplicate tracked entity values from database to history tables
        PropertyInfo[] properties = GetType().GetProperties();
        foreach (PropertyInfo property in properties.Where(p => p.PropertyType.IsGenericType
            && p.PropertyType.Name.StartsWith("DbSet")
            && p.PropertyType.GetGenericArguments().Length > 0
            && p.PropertyType.GetGenericArguments()[0].IsSubclassOf(typeof(TrackedEntity))))
        {
            foreach (TrackedEntity entity in (DbSet<TrackedEntity>)property.GetValue(this, null))
            {
                entity.Modified = DateTime.UtcNow;
                entity.ModifiedBy = username;
                entity.Version += 1;

                //Todo: duplicate entity values from database to history tables
            }
        }
        return base.SaveChanges();
    }

非常抱歉问题很长,这是一个相当复杂的问题。任何帮助将不胜感激。


我有同样的问题。我开始使用EF,我们也没有得出结论。任何帮助将不胜感激。 - Machado
把我的解决方案作为答案添加了进去,希望能够有所帮助。 - user1573618
3个回答

5

如果其他人想以同样的方式跟踪历史记录,这里是我采用的解决方案。我没有找到避免为每个被跟踪类创建单独历史记录类的方法。

我创建了一个基类,我的实体可以从中继承:

public abstract class TrackedEntity
{
    [Column(TypeName = "varchar")]
    [MaxLength(48)]
    [Required]
    public string ModifiedBy { get; set; }

    [Required]
    public DateTime Modified { get; set; }

    public int Version { get; set; }
}

对于每个实体,我创建一个普通实体类,但继承自我的基类:
public sealed class Record : TrackedEntity
{
    [Key]
    public int RecordID { get; set; }

    [MaxLength(64)]
    public string Name { get; set; }

    public int RecordTypeID { get; set; }

    [ForeignKey("RecordTypeID")]
    public virtual RecordType { get; set; }
}

对于每个实体,我还创建了一个历史记录类(始终是精确的副本,但将主键列移动,并删除所有外键)。
public sealed class Record_History : TrackedEntity
{
    [Key]
    public int ID { get; set; }

    public int RecordID { get; set; }

    [MaxLength(64)]
    public string Name { get; set; }

    public int RecordTypeID { get; set; }
}

最后,我在我的上下文类中创建了SaveChanges方法的重载,这将根据需要更新历史记录。

public class MyContext : DbContext
{
    ..........

    public int SaveChanges(string username)
    {
        //Set TrackedEntity update columns
        foreach (var entry in ChangeTracker.Entries<TrackedEntity>())
        {
            if (entry.State != EntityState.Unchanged && !entry.Entity.GetType().Name.Contains("_History")) //ignore unchanged entities and history tables
            {
                entry.Entity.Modified = DateTime.UtcNow;
                entry.Entity.ModifiedBy = username;
                entry.Entity.Version += 1;

                //add original values to history table (skip if this entity is not yet created)                 
                if (entry.State != EntityState.Added && entry.Entity.GetType().BaseType != null)
                {
                    //check the base type exists (actually the derived type e.g. Record)
                    Type entityBaseType = entry.Entity.GetType().BaseType;
                    if (entityBaseType == null)
                        continue;

                    //check there is a history type for this entity type
                    Type entityHistoryType = Type.GetType("MyEntityNamespace.Entities." + entityBaseType.Name + "_History");
                    if (entityHistoryType == null)
                        continue;

                    //create history object from the original values
                    var history = Activator.CreateInstance(entityHistoryType);
                    foreach (PropertyInfo property in entityHistoryType.GetProperties().Where(p => p.CanWrite && entry.OriginalValues.PropertyNames.Contains(p.Name)))
                        property.SetValue(history, entry.OriginalValues[property.Name], null);

                    //add the history object to the appropriate DbSet
                    MethodInfo method = typeof(MyContext).GetMethod("AddToDbSet");
                    MethodInfo generic = method.MakeGenericMethod(entityHistoryType);
                    generic.Invoke(this, new [] { history });
                }
            }
        }

        return base.SaveChanges();
    }

    public void AddToDbSet<T>(T value) where T : class
    {
        PropertyInfo property = GetType().GetProperties().FirstOrDefault(p => p.PropertyType.IsGenericType
            && p.PropertyType.Name.StartsWith("DbSet")
            && p.PropertyType.GetGenericArguments().Length > 0
            && p.PropertyType.GetGenericArguments()[0] == typeof(T));
        if (property == null)
            return;

        ((DbSet<T>)property.GetValue(this, null)).Add(value);
    }
    ..........
}

然后每当我保存更改时,我使用新方法,并传递当前用户名。我希望能够避免使用_History类,因为它们需要与主实体类一起维护,而且很容易被忘记。


0

谢谢您的建议。我应该在我的问题中说明,但我也想避免像这样将逻辑移动到数据库中。每次更改实体/表时,我仍然需要维护触发器。将来,我还需要生成大量数据库实例,因此最好让所有内容都在C#中处理,这样我只需运行迁移命令,让Entity Framework处理一切。 - user1573618

0
如果你正在使用SQL Server 2016或者Azure SQL,可以看一下时间表(系统版本化时间表)。

https://learn.microsoft.com/en-us/sql/relational-databases/tables/temporal-tables?view=sql-server-ver15

来自文档:

数据库功能,它内置支持提供有关任何时候存储在表中的数据的信息,而不仅仅是当前时间正确的数据。 Temporal 是一个数据库功能,它于 ANSI SQL 2011 中引入。

我编写了一份完整的指南,介绍如何在 Entity Framework Core 中实现它,而无需使用任何第三方库。也应该可以使用 Entity Framework,但尚未经过测试。

https://stackoverflow.com/a/64244548/3850405


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接