[英]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
主表和歷史表中添加了兩個字段:
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.