【问题标题】:Capture the user who deleted the row in Temporal table捕获删除临时表中行的用户
【发布时间】:2020-06-08 13:39:12
【问题描述】:

我了解时态表旨在为您提供数据的时间点视图。我正在使用临时表进行审计。我有以下时态表。

让我们假设这是 Temporal 表的当前状态:

ID  RoleID  UserID      ModifiedBy
------------------------------------------
1   11      1001        foo@example.com
2   22      1001        foo@example.com
3   33      1002        bar@example.com
4   11      1003        foo@example.com

我有一个使用 EF Core 的 Web 应用程序。我的 EF 代码总是将 ModifiedBy 设置为当前登录的用户。我以bar@example.com 登录应用程序并删除了一条ID 为2 的记录。SQL Server 将按预期自动将删除的记录插入历史记录表中,并将ModifiedBy 保持为foo@example.com,因为那是ModifiedBy 列的时间点值。

但是现在系统不知道是谁删除了该行。在这种情况下,bar@example.com 是实际删除该行的人。如何捕获删除记录的用户?我在这里有什么选择?

【问题讨论】:

  • 也许,添加一个通常为 Null 的 DelCode 列,并且在处理删除事务时,proc 将值更新为 1,从而导致 ModifiedBy 使用您的 UserID 进行更新,并且记录存储在历史记录中DelCode=1 和 ModifiedBy=yourUserID
  • 您可以在历史记录表中添加 DeletedBy 列并在删除时填充它。
  • @Nikolaus 你不能直接更新修改历史表
  • @donPablo 我没有使用存储过程。我正在使用 EF。
  • 出于这个原因,我们使用软删除,因为它有一个“IsDeleted”位列。因此,要删除记录,我们只需设置 IsDeleted 并捕获 ModifiedBy 值。在 EF 中,您可以将其配置为在对表的任何查询中排除软删除的记录。

标签: sql-server tsql ef-core-2.2 temporal-tables


【解决方案1】:

正如您正确提到的,系统的行为符合预期,即历史表根据系统时间存储来自临时表的最新行(完整行)(通过传输)(参考:https://docs.microsoft.com/en-us/sql/relational-databases/tables/temporal-tables?view=sql-server-ver15) .但是,您似乎想在此类转移交易发生之前覆盖 ModifiedBy Column 中的值。由于这违反了审计的核心原则(每笔交易的内容、时间和对象),因此系统不允许它发生是正确的。

一个建议是更改上下文中的原始表,以增加一列来映射当前 userId(参考:Default logged in user to SQL Server column)。这可以通过在数据库本身中更改表结构或通过修改 EF 代码以添加新列并存储当前用户来实现。

【讨论】:

  • 要在删除不违反审计的情况下立即修改记录,此时用户 X 正在对行进行更改,记录该事实正是审计日志应该做的。以前的值仍然保留在以前的日志中,所以在这种情况下我们没有污染任何东西。
【解决方案2】:

审计删除本身就有问题,但在 EF 或用户没有自己与数据库的安全连接的断开连接的环境中跟踪和存储 当前用户 也是如此,所以我们不能使用数据库用户来确定当前用户是谁。

虽然实现“软”删除是避免这种情况的一种选择,但它引入了一种新的结构依赖性(行状态标志),ALL 会针对受影响的表查询以考虑行状态标志。这不是一件坏事,但它对整个应用程序运行时产生了重大变化,包括可能不使用 EF 上下文的任何临时维护、报告和数据集成。

看看CONTEXT_INFO,在 DBA 帖子的这个答案中有一篇很好的文章:Passing info on who deleted record onto a Delete trigger

如果您愿意,使用CONTEXT_INFO 允许我们将用户审计管理从 EF 代码移动到数据库中,这样做的好处是您的审计现在将拾取由与数据库的所有交互执行的修改,而不仅仅是EF申请流程。

有一篇关于在 EF 中设置 CONTEXT_INFO 的旧帖子仍然主要适用:Entity Framework and User Context

  1. 创建一个 SP 来设置 CONTEXT_INFO,这是一个 128 字节的值。
    将此添加到您的迁移脚本中

     Create Procedure [dbo].[SetEFUserContext]
     (
         @UserID int,
         @UserName Varchar(100)
     )
     AS
     Begin
     Begin Try
         Declare @CONTEXT_INFO Varbinary(max)
    
         SET @CONTEXT_INFO =cast('UserId='+CONVERT(varchar(10),@UserID)
         +',Name=' + RTrim(@UserName)
         +REPLICATE(' ',128) as varbinary(128))
    
         SET CONTEXT_INFO @CONTEXT_INFO
    
     End Try
     Begin Catch
         Declare @Errmsg Varchar(max),@ErrSeverity int
         Set @Errmsg=ERROR_MESSAGE()
         Set @ErrSeverity=ERROR_SEVERITY()
         Raiserror(@Errmsg,@ErrSeverity,1)
     End Catch
     End
    
  2. 在你的 DbContext 中重写 SaveChanges() 方法,在每次数据库模型更改之前执行上述 SP:

     public override int SaveChanges()
     {           
         SetUserContext();
    
         return base.SaveChanges();
     }
    
     public int UserId
     {
         // Implement your own logic to resolve the current user id
         get; set;
     }
    
     public int UserName
     {
         // Implement your own logic to resolve the current user name
         get; set;
     }
    
     private void SetUserContext ()
     {
         if (String.IsNullOrWhiteSpace(UserName))
             return;
    
         //Open a connection to the database so the session is set up
         this.Database.Connection.Open();
    
         //Set the user context
         //Cannot use ExecuteSqlCommand here as it will close the connection
         using (var cmd = this.Database.Connection.CreateCommand())
         {
             var userNameParam = cmd.CreateParameter();
             userNameParam.ParameterName = "@userName";
             userNameParam.Value = UserName;
    
             var userIdParam = cmd.CreateParameter();
             userIdParam.ParameterName = "@userId";
             userIdParam.Value = UserId;
    
             cmd.CommandText = "SetEFUserContext";
             cmd.CommandType = System.Data.CommandType.StoredProcedure;
             cmd.Parameters.Add(userIdParam);
             cmd.Parameters.Add(userNameParam);
    
             cmd.ExecuteNonQuery();
         };
     }
    
  3. 在删除行之前在表上使用触发器来修改行。这样,最终用户将更新到该行中,并且您现有的时间逻辑应该保留该用户。

    注意:这个必须为每个表配置,你可以从EF编写脚本,或者在SQL中编写一个SP来生成它们,以下只是为了演示在单个表上的使用叫table1

     CREATE TRIGGER auditTemporalDeleteTrigger
         ON database1.dbo.table1
         FOR DELETE
     AS
         DECLARE @user VARCHAR(100), @userId int;
         SELECT @user = SYSTEM_USER
    
         -- support for domain credentials, omit the domain name
         IF(CHARINDEX('\', @user) > 0)
             SELECT @user = SUBSTRING(@user, CHARINDEX('\', @user) + 1, 25);
         SELECT @user = SUBSTRING(@user, 1, 100);
    
         --To support EF or web apps with single shared connection, use Context_info
         DECLARE @sCONTEXT_INFO varchar(128) = (SELECT CAST(CONTEXT_INFO() AS VARCHAR) FROM sys.SYSPROCESSES WHERE SPID =@@SPID )
         IF @sCONTEXT_INFO like '%UserId%'
         BEGIN
             SELECT @userId = Substring(@sCONTEXT_INFO, CHARINDEX('UserId=', @sCONTEXT_INFO) + 7, CHARINDEX(',', @sCONTEXT_INFO, CHARINDEX('UserId=', @sCONTEXT_INFO)) - CHARINDEX('UserId=', @sCONTEXT_INFO) - 7)
             SELECT @User = RIGHT(RTRIM(@sCONTEXT_INFO), LEN(RTRIM(@sCONTEXT_INFO)) - CHARINDEX('Name=', @sCONTEXT_INFO) - 5 + 1)-- + 1 due to RIGHT function and CHARINDEX
         END
    
         -- Update the record before the delete, to affect the logs
         UPDATE table1
         SET ModifiedBy = @User, UserID = @userId     
         WHERE ID IN (SELECT ID FROM deleted);
    
         -- Actually perform the delete now
         DELETE FROM table1
         WHERE ID IN (SELECT ID FROM deleted);
    
     GO
    

如果您沿着这条路线走,那么为插入和更新实现 AFTER 触发器并不费力,因此您可以维护 ModifiedByUserID 列,而不必污染您的 EF 运行时不仅仅是设置CONTEXT_INFO

您可以将其写入您的迁移脚本生成逻辑,或者像我现在所做的那样,您可以编写一个 SP 来为您要跟踪审计的所有表生成和维护触发器。

此建议同样适用于自定义审计日志记录,除了您可以拥有一个涵盖 AFTER insertupdatedelete 的触发器,而无需手动拦截像我们在这里所做的那样执行DELETE

我尝试在 SqlFiddle 中对此进行模拟,但内存不足。一开始我并不相信它,但是像这样的FOR DELETE 触发器非常适合临时表!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多