使用Entity Framework的快照历史记录

17

我一直在研究Entity Framework的审计钩子。其中许多钩子显示旧/新值比较。这对于审计跟踪非常有用,但我想要的是快照对象。

例如... 假设我有一个管理产品的应用程序。 一个产品有多个属性和相关的其他对象。 假设我更改了一个对象10次。 我还想查看那些对象变化屏幕(不是审计跟踪,而是以只读格式实际显示的屏幕)。 我感兴趣的是能够检索所有10个更改(取决于我想要查看哪个)的原始EF产品对象(具有所有相关数据),并将其用于绑定到我的屏幕。

如果我使用SQL Server,现在应该使用什么类型的序列化对象(XML、blob等)? 这样做有意义吗?


我会添加奖励以获取更详细的回答,请 :) - Anyname Donotcare
1
对于您想要存储历史记录的每个表:1)添加版本列(可以是时间,也可以是递增计数器)。2)添加另一个表,具有与主表相同的所有属性(但外键更好扩展)。3)在更新之前的触发器中,如果版本列已更改-将旧值复制到此版本表中。4)获利。 - Evk
@Evk:如果您能添加一个详细的答案,并提供一个使用EF作为企业应用程序基础的简单示例(涉及删除操作和多对多关系),我将非常感激。 - Anyname Donotcare
4个回答

12

让我们来看看。您需要将对象图序列化为数据库中的格式,以便稍后可以恢复它。我认为确实有一些工具可以做到这一点。其中一个工具是Entity Framework。

您想做的事情非常普遍。考虑一个维基引擎。维基需要有一个每个人都可以看到的提示版本,以及每个文档的后备版本。维基还需要能够以与提示版本相同的方式显示后备版本。因此,应该使用相同的存储格式。

我建议允许所有实体类型进行版本控制。当您编辑实体类型时,将编辑提示版本并存储包含先前值的后备版本。(之所以编辑提示版本而不是插入新提示是因为其他未当前材料化为ObjectContext的对象可能包含链接到提示的链接,您希望将其保留为链接到提示,而不是链接到后备版本。)

如有必要,可以对 SQL Server 表进行分区,以使后备版本存储在不同的文件组中。这将允许您单独备份提示版本和后备版本。


我建议您允许所有实体类型进行版本控制。您能否详细说明一下您的意思? - Anyname Donotcare

5

首先,您需要向表中添加一组属性:

  • Version - 最后修改时间(也可以是自动递增计数器)。
  • LastModifiedBy - 最后修改的用户的引用(如果您存储了该信息)。

然后,您有几个选项来存储版本历史记录。您可以:

  1. 为您想要存储历史记录的每个主要表创建一个新表。该历史记录表将具有与主表相同的所有字段,但不会强制执行主键和外键。对于每个外键,还要存储引用条目的版本(在创建版本时)。

  2. 或者,您可以序列化与实体相关的所有有趣内容,并将所有这些序列化的 blob 存储在一个全局历史记录表中(我个人更喜欢第一种方法)。

如何填充历史记录表?通过更新和删除触发器。

  • 在实体的更新触发器中 - 将所有先前的值复制到历史记录表中。对于每个外键 - 还要复制引用实体的当前版本。
  • 在删除触发器中 - 基本上做同样的事情。

请注意,越来越多的现代系统实际上不会真正删除任何内容。他们只是将事物标记为已删除。如果您想遵循此模式(它有几个好处) - 请向实体添加 IsDeleted 标志(当然,您必须在所有地方过滤已删除的实体)。

如何查看历史记录?只需使用历史记录表,因为它具有与主表相同的所有属性 - 不应该有问题。但是 - 在扩展外键时 - 确保引用实体的版本与您在历史记录表中存储的版本相同。如果不是 - 您需要转到该引用实体的历史记录表并在那里获取值。这样,您将始终拥有实体在那个时刻的快照,包括所有引用。

除了以上所有内容之外 - 您还可以将实体的状态恢复到任何以前的版本。

请注意,这种实现虽然简单,但可能会消耗一些空间,因为它存储的是快照,而不仅仅是正在进行的更改。如果您只想存储更改 - 在更新触发器中,您可以检测到哪些字段已更改,将它们序列化并存储在全局历史记录表中。这样,您至少可以在用户界面上显示已更改的内容以及由谁更改(尽管您可能会遇到恢复到某个以前版本的问题)。


如果我想通过'EF'处理触发器逻辑,那么它应该在事务中吗? - Anyname Donotcare
1
好的,当然可以(但请记住,SaveChanges已经在事务内运行,因此如果您将历史记录条目添加到上下文中并在那里应用更新-您已经在事务中执行了该操作)。 - Evk
不确定您所说的“每个表有两个属性”是什么意思。如果这是关于外键的问题 - 我的意思是在_history_表中存储引用键的版本,因为引用实体也可能会更改。假设您有一个引用PersonInfo的Person表,而PersonInfo又具有Address字段。当您保存Person表的历史记录时,您需要(在历史记录表中)保存PersonInfoID(对PersonInfo的外键)和PersonInfo.Version,因为稍后PersonInfo可能会更改,您需要知道它在保存其状态时的状态。 - Evk
假设您只序列化Person表中的所有属性并将其作为Blob存储。 然后,您将无法以后返回该版本,因为在此期间任何相关表中的任何内容都可能已更改。 再次强调 - 只有当您想要恢复时才采用此方法,否则您希望仅序列化已更改的列,并在必要时向用户显示。 - Evk
1
顺便提一下,如果你需要更简单的东西并且使用 EF - 可以看看这里 https://github.com/loresoft/EntityFramework.Extended,它有 AuditLog 功能。 - Evk
显示剩余6条评论

3
在我最近构建的项目中,我们使用了DbContext类中的SaveChanges方法。这使我们可以访问ChangeTracker类的实例。调用ChangeTracker.Entries()会给您访问DbEntityEntry列表的权限。 DbEntityEntry具有以下有趣的属性和方法:
  • State - 对象是新创建的,已修改或正在被删除
  • Entity - 对象的副本
  • CurrentValues - 编辑值的枚举
  • OriginalValues - 原始值的枚举
我们创建了一组POCOs用于更改集和更改,然后通过EF进行访问。这允许我们的用户查看字段级别的更改以及日期和负责人。

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 中实现它,而不需要使用任何第三方库,详见此处:

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


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