在之前一篇介绍CDC的文章中,我说Audit Trail(或者Audit Log)是大部分企业级应用不可以或缺的功能。本篇给你一个完整的Audit Trail解决方案,不仅可以记录每一笔业务操作的信息(比如操作时间、操作者等),并且可以追踪每一笔业务引起的说有数据的改变(如果需要)。
目录
一、数据表的设计
二、数据变化的表示
三、AuditLog基本信息的写入
四、通过SQLCDC追踪源表数据变化
五、删除操作的TransactionId如何被记录?
六、通过SQL Job转储AuditLog详细信息
七、代码生成的应用
一、数据表的设计
在数据库中,我们通过如右图所示的具有主子关系的两个表存储AuditLog相关信息。我们将“事务”作为我们进行追踪的单位,不过这里的讲的“事务”更多地指业务处理事务的概念。每一个被追踪的事务在AuditLog表具有一条匹配的记录,该记录表示该事务的基本信息:UserName(操作者)、AuditTime(操作时间)、Activity(可以看成是对事物的命名)和Description(事务补充性的描述)。主键TransactionId唯一标识一个事务。
子表AuditLogData记录事务详细的信息,即事务所引起的数据变化。一个完整的业务逻辑往往涉及到对多个数据表、多条记录的操作。而AuditLogData每一条记录表示某个事务针对某个单一数据表所带来的数据变化,而SourceTable字段表示源表的名称。而DataChange字段以XML的形式表示数据的改变,它具有如下的格式。
二、数据变化的表示
数据操作类型无外乎添加、更新和删除,我们通过不同的XML结构表示不同操作引起的数据改变。具体来说,对于添加操作,我们需要记录下插入的记录;对于删除操作,需要记录下原来的记录;而对于数据更新,则需要同时记录下更新先后的记录。
举个例子,假设我们具有一个Users表,它具有三个基本字段:Id、Name和Birthday。下面的XML分别表示添加、删除和更新操作后我们需要记录下的数据变化。
添加:
1: <?xml version="1.0" encoding="utf-8" ?>
2: <cdc operation="insert">
3: <current>
4: <Id type="VARCHAR(50)">001</Id>
5: <Name type="NVARCHAR(50)">Foo</Name>
6: <BirthDay type="DATE">1981-08-24</BirthDay>
7: </current>
8: </cdc>
删除:
1: <?xml version="1.0" encoding="utf-8" ?>
2: <cdc operation="delete">
3: <original>
4: <Id type="VARCHAR(50)">001</Id>
5: <Name type="NVARCHAR(50)">Foo</Name>
6: <BirthDay type="DATE">1981-08-24</BirthDay>
7: </original>
8: </cdc>
更新:
1: <?xml version="1.0" encoding="utf-8" ?>
2: <cdc operation="update">
3: <original>
4: <Id type="VARCHAR(50)">001</Id>
5: <Name type="NVARCHAR(50)">Foo</Name>
6: <BirthDay type="DATE">1981-08-24</BirthDay>
7: </original>
8: <current>
9: <Id type="VARCHAR(50)">001</Id>
10: <Name type="NVARCHAR(50)">Bar</Name>
11: <BirthDay type="DATE">1982-07-10</BirthDay>
12: </current>
13: </cdc>
当然,你也可以根据需要自定义XML的结构。
三、AuditLog基本信息的写入
我们现在我们的目标就是如何将追踪到的基于一个事务相关的信息写入到上面我们创建的两个表中。主表AuditLog的信息是很容易被写入的,比如你可以定义像下面一样的一个AuditLogger类。
1: public class AuditLogger
2: {
3: public void Write(string activity)
4: { }
5:
6: public void Write(string activity, string description)
7: { }
8: }
AuditLogger的Write方法进行传入了Activity和Description,而没有TransactionId、UserName和AuditTime。其中AuditTime自然是当前时间,而UserName应该是登录系统的用户。而对于TransactionId,我们应该采用上下文的方式来获取,具体原因会在下面谈到。如果你直接使用System.Transactions事务实现我们进行追踪的“事务”,你可以直接使用当前事务(Transaction.Current)的DistributedIdentifier或者LocalIdentifier。
1: var transactionId = Transaction.Current.TransactionInformation.DistributedIdentifier;
2: //Or
3: var transactionId = Transaction.Current.TransactionInformation.LocalIdentifier;
基于AuditLog表的事务基本信息的日志好解决,那么我们如何将事务引起的事务变化记录到AuditLogData表中呢?这样的工作我们完全实现在SQL Server中。
四、通过SQLCDC追踪源表数据变化
《追踪记录每笔业务操作数据改变的利器——SQLCDC》介绍了一种有效记录基于某个数据表数据变化的方式:SQLCDC,在这里我们直接利用它来记录AuditLog的详细信息。当我们为某个表(比如Users)开启了CDC特性之后,SQL Server会为之创建一个相应的CT表(Users_CT),在默认的情况下Users_CT包含与Users表的所有字段。如果你不希望CDC追踪所有的字段,你可以显式地设定具体的字段。
AuditLogData表中有一个字段TransactionId表示记录属于哪个具体的事务,为了让CDC可以记录下正确TransactionId,需要在每一个被追踪的表中添加这么一个额外的字段。这个应该不是什么问题,比如我们的每个表中都具有6个系统字段:TransactionId、VersionNo、CreatedBy、CreatedTime、LatestUpdatedBy和LatestUpdatedTime。
由于每个数据表都具有了一个TransactionId字段,那么在进行数据提交的时候,需要将当前事务的ID为之赋值,这就是为什么我推荐采用上下文的方式来获取当前TransactionId的原因。但是,还有一个问题没有解决——数据删除操作的TransactionId如何被记录下来呢?
五、删除操作的TransactionId如何被记录?
由于代表当前事务的TransactionId最终会通过Insert或者Update SQL语句写入数据表,但是对于删除操作呢?由于我们直接调用Delete语句将相应的数据操作,表示当前删除操作所在的事务是无法被写入的,最终CDC记录下来的数据是无法反映出删除的记录隶属于哪个事务。
由于最终对数据库操作都是通过SQL提交的,或者是存储过程,或者是SQL文本。为了解决这个问题,我们只需要改变我们的SQL脚本,在Delete执行之前执行Update语句写入新的TransactionId。
也就是说,对于一个删除操作,实际上是先做Update,最后做Delete。在这种情况下,CDC会为你记录下三条记录,前两条是为Update记录的,最后一条是为Delete记录的。为了区分CDC追踪的记录是正常的Update还是为了Delete而进行的Update,我们可以做一些标记。比如你可以在TransactionId的值之前添加一个前缀,表示Update操作是为Delete而作的。
六、通过SQL Job转储AuditLog详细信息
CDC仅仅会将基于某个表的数据改变记录到基于该表的CT表中,最终我们需要将这些CT表中的数据转存到我们指定的AuditLogData表中,这个工作可以通过SQLJob来实现。你自行创建一个SQL Job实现从若干CT表到AuditLogData的数据转存,并根据你的需要(主要是实时性的需要)配制Job执行的时间或者间隔。右图揭示了AuditLog详细信息是如何一步步地被记录的。
七、代码生成的应用
在这个解决方案中,我们需要一个不可或缺的东西:代码生成器。它用于自动生成如下的SQL脚本:为某个表开启CDC特性并指定追踪字段的T-SQL脚本,和进行AuditLog详细信息转存(丛CT表到AuditLogData表)的SQL Job脚本。关于代码生成,可以参考《与VS集成的若干种代码生成解决方案》