繁体   English   中英

SQL Server 2016 - 时态表 - 如何识别用户

[英]SQL Server 2016 - Temporal Table - how to identify the user

是否可以获取有关修改历史表中数据的用户/连接的信息? 我阅读了有关可以使用时态表的审计方案,并且可以检测到谁更改了数据。 但是我该怎么做呢?

一个看似无懈可击的审计解决方案,它给出了进行每次更改的登录用户的名称(以及我之前在此页面上的回答的巨大改进):

SELECT 
  e.EmployeeID, e.FirstName, e.Score, 
  COALESCE (eh.LoggedInUser, o.CreatedBy, e.CreatedBy) AS CreatedOrModifiedBy, 
  e.ValidFromUTC, e.ValidToUTC

FROM dbo.Employees FOR SYSTEM_TIME ALL AS e
  LEFT JOIN dbo.EmployeeHistory AS eh    -- history table
    ON e.EmployeeID = eh.EmployeeID AND e.ValidFromUTC = eh.ValidToUTC
    AND e.ValidFromUTC <> eh.ValidFromUTC

OUTER APPLY
  (SELECT TOP 1 CreatedBy 
   FROM dbo.EmployeeHistory 
   WHERE EmployeeID = e.EmployeeID 
   ORDER BY ValidFromUTC ASC) AS o     -- oldest history record

--WHERE e.EmployeeID = 1
ORDER BY e.ValidFromUTC

结果集

  • 不使用触发器或用户定义的函数
  • 需要对表进行小的更改
  • 注意:请注意,SQL Server 始终使用UTC而不是本地时间作为时态表中的时间戳。
  • 编辑: (2018/12/03)(感谢@JussiKosunen!)当同一条记录同时发生多个更新时(例如在一个事务中),只返回最新的更改(见下文)

解释:

主表和历史表中添加了两个字段:

  • 记录创建记录的用户的姓名 - 一个普通的 SQL 默认值:
    CreatedBy NVARCHAR(128) NOT NULL DEFAULT (SUSER_SNAME())
  • 随时记录当前登录用户的姓名。 一个计算列:
    LoggedInUser AS (SUSER_SNAME())

当一条记录插入到主表中时,SQL Server 不会向历史表中插入任何内容。 但是字段CreatedBy记录谁创建了记录,因为默认约束。 但是,如果/当记录更新时,SQL Server 会在关联的历史记录表中插入一条记录。 这里的关键思想是将进行更改的登录用户名称记录到历史表中,即主表​​中字段LoggedInUser的内容(它始终包含登录到连接的用户的名称)保存到历史表中的LoggedInUser字段中。

这几乎是我们想要的,但不完全是——这是一个变化。 例如,如果用户 Dave 插入了记录,但用户 Andrew 进行了第一次更新,则“Andrew”作为用户名记录在历史表中,紧挨着 Dave 插入的记录的原始内容。 然而,所有的信息都在那里——它只需要被解开。 加入系统为 ROW START 和 ROW END 生成的字段,我们得到进行更改的用户(来自历史表中的前一条记录)。 但是,历史记录表中没有记录的原始插入版本的记录。 在这种情况下,我们检索CreatedBy字段。

这似乎提供了一个无懈可击的审计解决方案。 即使用户编辑了CreatedBy字段,该编辑也会记录在历史表中。 出于这个原因,我们从历史表中恢复CreatedBy的最旧值,而不是从主表中恢复当前值。

删除的记录

上面的查询没有显示谁从主表中删除了记录。 这可以使用以下方法检索(可以简化吗?):

SELECT 
  d.EmployeeID, d.LoggedInUser AS DeletedBy, 
  d.CreatedBy, d.ValidFromUTC, d.ValidToUTC AS DeletedAtUTC
FROM
  (SELECT EmployeeID FROM dbo.EmployeeHistory GROUP BY EmployeeID) AS eh   -- list of IDs
OUTER APPLY
  (SELECT TOP 1 * FROM dbo.EmployeeHistory 
   WHERE EmployeeID = eh.EmployeeID 
   ORDER BY ValidToUTC DESC) AS d -- last history record, which may be for DELETE
LEFT JOIN
  dbo.Employees AS e
    ON eh.EmployeeID = e.EmployeeID
WHERE e.EmployeeID IS NULL          -- record is no longer in main table

示例表脚本

以上示例基于表脚本(历史表由SQL Server创建):

CREATE TABLE dbo.Employees(
  EmployeeID INT /*IDENTITY(1,1)*/ NOT NULL,
  FirstName NVARCHAR(40) NOT NULL,
  Score INTEGER NULL,
  LoggedInUser AS (SUSER_SNAME()),
  CreatedBy NVARCHAR(128) NOT NULL DEFAULT (SUSER_SNAME()),
  ValidFromUTC DATETIME2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL DEFAULT SYSUTCDATETIME(),
  ValidToUTC DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL DEFAULT CAST('9999-12-31 23:59:59.9999999' AS DATETIME2),
  CONSTRAINT PK_Employees PRIMARY KEY CLUSTERED (EmployeeID ASC),
  PERIOD FOR SYSTEM_TIME (ValidFromUTC, ValidToUTC)
) 
WITH (SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.EmployeeHistory ))

编辑: (2018 年 11 月 19 日)添加了针对 system_time 字段的默认约束,一些人认为这是最佳实践,如果您将系统版本控制添加到现有表中会有所帮助。

编辑: (2018/12/03)根据@JussiKosunen 的评论更新(感谢 Jussi!)。 请注意,当多个更改具有相同的时间戳时,查询仅返回当时的最后一个更改。 以前它为每个更改返回一行,但每个都包含最后一个值。 寻找一种方法使其返回所有更改,即使它们具有相同的时间戳。 (请注意,这是一个真实世界的时间戳,而不是为了避免破坏物理世界而被弃用的“ Microsoft 时间戳”。)

编辑: (2019/03/22)修复了查询中显示已删除记录的错误,在某些情况下它会返回错误的记录。

编辑:在本页的其他地方看到我更好的答案

我的解决方案不需要触发器。 我在主表中有一个计算列,它总是包含登录用户,例如

CREATE TABLE dbo.Employees(
    EmployeeID INT NOT NULL,
    FirstName sysname NOT NULL,
    ValidFrom DATETIME2(7) GENERATED ALWAYS AS ROW START NOT NULL,
    ValidTo DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL,
    LoggedInUser AS (SUSER_SNAME()),  --<<-- computed column

... 等等。

LoggedInUser字段始终包含每个记录中当前登录用户的名称,因此对任何记录进行任何更改时保存到历史表

当然,这在主表中不是很有用,因为它没有显示谁对每条记录进行了最后一次更改。 但是在历史记录表中,它在进行更改时被冻结,这非常有用,(尽管它在周期结束时记录用户,而不是开始时)。

注意,作为计算列, LoggedInUser必须可以为空,因此历史表中对应的列也必须可以为空。

主(当前)表: 主桌

历史表: 历史表

当然,在历史表,它记录谁该状态改变的记录,还没那个状态,即登录的用户在有效期结束。 它也适用于删除,但 SQL Server 时态表系统不会在历史表中插入记录以进行插入。

欢迎任何有关如何改进这一点的想法,例如,如何在历史表中的每个有效期开始时记录谁进行了更改。 我有一个想法,涉及主表中的另一个计算字段,它使用 UDF 来获取在历史记录表中进行最后更改的用户。

编辑:我从@Aaron Bertrand 在这里使用触发器的优秀文章中找到了很多灵感。

在时态表的当前实现中,它只记录基于时间的信息,而没有关于进行更改的会话的其他信息。 不要因为我知道这种情况在未来可能会发生变化而阅读该声明; 我对此一无所知。 如果您需要该信息,则需要将其记录在一行中。 执行此操作的经典方法是使用触发 DML 操作并代表用户维护该值的触发器。

我正在考虑解决此问题的另一个选项是在您的基本时态表中有一个 LastModifiedBy 字段,该字段在保存或更新行时填充。

这将显示谁修改了表并因此创建了历史记录。 正如 Aaron 上面提到的,你可以在触发器中完成它,但我的想法是在它到达插入/更新之前决定它并在更新记录时将值放在 LastModifiedBy 字段中。

每次修改记录时,这也将在历史表中。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM