2012-09-24 341 views
73

我被問及是否可以跟蹤對MySQL數據庫中記錄的更改。所以當一個領域被改變時,舊的vs新的可用的和發生的日期。有沒有一個功能或共同的技術來做到這一點?是否有MySQL選項/功能來跟蹤記錄更改的歷史記錄?

如果是這樣,我正在考慮做這樣的事情。創建一個名爲更改的TABLE。它將包含與主TABLE相同的字段,但前綴爲舊的和新的,但僅限於實際更改的那些字段以及一個TIMESTAMP。它將被編入一個ID。這樣,可以運行一個SELECT報告來顯示每條記錄的歷史記錄。這是一個好方法嗎?謝謝!

+0

的可能的複製[如何保持記錄更新的歷史在MySQL?(http://stackoverflow.com/questions/2536819/ how-to-keep-history-of-record-updates-in-mysql) – Gajus

回答

47

這很微妙。

如果業務需求是「我想審覈對數據的更改 - 誰做了什麼和什麼時間?」,通常可以使用審計表(根據Keethanjan發佈的觸發器示例)。我不是一個觸發器的粉絲,但它具有實現相對無痛的巨大好處 - 您現有的代碼不需要知道觸發器和審計內容。

如果業務需求是「向我顯示過去某個特定日期的數據狀態」,則意味着隨着時間的推移已經進入您的解決方案。雖然你可以通過查看審計表來重建數據庫的狀態,但它很難且容易出錯,而且對於任何複雜的數據庫邏輯來說,它變得很笨拙。例如,如果企業想要知道「找到我們應該發送給在本月的第一天有優秀未付帳單的客戶的地址」,那麼您可能需要翻查六張審計表。相反,您可以將隨時間變化的概念烘焙到您的模式設計中(這是Keethanjan建議的第二種選擇)。這是對應用程序的改變,絕對是在業務邏輯和持久層面上的改變,所以它不是微不足道的。

舉例來說,如果你有一個像這樣的表:

CUSTOMER 
--------- 
CUSTOMER_ID PK 
CUSTOMER_NAME 
CUSTOMER_ADDRESS 

和你想跟蹤隨着時間的推移,你可以按如下修改它:

CUSTOMER 
------------ 
CUSTOMER_ID   PK 
CUSTOMER_VALID_FROM PK 
CUSTOMER_VALID_UNTIL PK 
CUSTOMER_STATUS 
CUSTOMER_USER 
CUSTOMER_NAME 
CUSTOMER_ADDRESS 

你不想每次都更改客戶記錄,而不是更新記錄,您將當前記錄上的VALID_UNTIL設置爲NOW(),並插入帶有VALID_FROM(現在)和空VALID_UNTIL的新記錄。您將「CUSTOMER_USER」狀態設置爲當前用戶的登錄ID(如果需要保留該狀態)。如果客戶需要刪除,您可以使用CUSTOMER_STATUS標誌來表明這一點 - 您可能永遠不會從該表中刪除記錄。

通過這種方式,您可以隨時查找客戶表在給定日期的狀態 - 地址是什麼?他們改名了嗎?通過加入其他具有類似valid_from和valid_until日期的表,您可以重建歷史上的整個圖片。要查找當前狀態,請搜索VALID_UNTIL日期爲空的記錄。 (嚴格來說,你不需要valid_from,但它使查詢更容易一些)。它使您的設計和數據庫訪問複雜化。但它使重建世界變得更容易。

+0

但是它會爲那些沒有更新的字段添加重複數據?如何管理它? – itzmukeshy7

+0

如果在一段時間內對客戶記錄進行編輯,則難以識別特定條目是屬於同一客戶還是屬於不同客戶,因此報告生成會出現第二種方法問題。 –

+0

最好的建議我已經看到這個問題 – Worthy7

13

你可以創建觸發器來解決這個問題。 Here is a tutorial to do so(存檔鏈接)。

設置約束和規則在數據庫中比寫 特殊的代碼來處理同樣的任務,因爲它會阻止另一 開發商從寫繞過所有 特殊代碼的不同的查詢更好,可以讓你的數據庫數據完整性較差。

很長一段時間,我使用腳本 將信息複製到另一個表,因爲MySQL當時不支持觸發器。我現在發現這個觸發器在跟蹤一切時更有效。

如果某個人修改了某行,則該觸發器會將舊值複製到歷史記錄表中,如果該值已更改 。每次有人編輯該行時,Editor IDlast mod都存儲在 原始表格中;時間對應於 ,直到它被改變爲當前的形式。

DROP TRIGGER IF EXISTS history_trigger $$ 

CREATE TRIGGER history_trigger 
BEFORE UPDATE ON clients 
    FOR EACH ROW 
    BEGIN 
     IF OLD.first_name != NEW.first_name 
     THEN 
       INSERT INTO history_clients 
        (
         client_id , 
         col   , 
         value  , 
         user_id  , 
         edit_time 
        ) 
        VALUES 
        (
         NEW.client_id, 
         'first_name', 
         NEW.first_name, 
         NEW.editor_id, 
         NEW.last_mod 
        ); 
     END IF; 

     IF OLD.last_name != NEW.last_name 
     THEN 
       INSERT INTO history_clients 
        (
         client_id , 
         col   , 
         value  , 
         user_id  , 
         edit_time 
        ) 
        VALUES 
        (
         NEW.client_id, 
         'last_name', 
         NEW.last_name, 
         NEW.editor_id, 
         NEW.last_mod 
        ); 
     END IF; 

    END; 
$$ 

另一種解決辦法是保持一個版本域和更新保存這個領域。你可以決定max是最新的版本,或者0是最新的版本。這取決於你。

115

下面是做到這一點的簡單方法:

首先,創建一個歷史表爲您想要跟蹤(下面的例子查詢),每個數據表。此表將爲每個在數據表中每行執行的插入,更新和刪除查詢都提供一個條目。

歷史表的結構將與其追蹤的數據表相同,除了三個附加列:存儲發生的操作的列(我們稱之爲「操作」),操作的日期和時間,以及存儲序列號('修訂版')的列,該序列號按每個操作遞增,並按數據表的主鍵列進行分組。

要執行此排序行爲,將在主鍵列和修訂列上創建一個雙列(複合)索引。請注意,如果歷史表使用的引擎是MyISAM,則只能按此方式進行排序(See 'MyISAM Notes' on this page)

歷史記錄表相當容易創建。在下面的ALTER TABLE查詢中(以及在下面的觸發器查詢中) ,替換「primary_key_column」在你的數據表中列的實際名稱

CREATE TABLE MyDB.data_history LIKE MyDB.data; 

ALTER TABLE MyDB.data_history MODIFY COLUMN primary_key_column int(11) NOT NULL, 
    DROP PRIMARY KEY, ENGINE = MyISAM, ADD action VARCHAR(8) DEFAULT 'insert' FIRST, 
    ADD revision INT(6) NOT NULL AUTO_INCREMENT AFTER action, 
    ADD dt_datetime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER revision, 
    ADD PRIMARY KEY (primary_key_column, revision); 

,然後創建觸發器:。

DROP TRIGGER IF EXISTS MyDB.data__ai; 
DROP TRIGGER IF EXISTS MyDB.data__au; 
DROP TRIGGER IF EXISTS MyDB.data__bd; 

CREATE TRIGGER MyDB.data__ai AFTER INSERT ON MyDB.data FOR EACH ROW 
    INSERT INTO MyDB.data_history SELECT 'insert', NULL, NOW(), d.* 
    FROM MyDB.data AS d WHERE d.primary_key_column = NEW.primary_key_column; 

CREATE TRIGGER MyDB.data__au AFTER UPDATE ON MyDB.data FOR EACH ROW 
    INSERT INTO MyDB.data_history SELECT 'update', NULL, NOW(), d.* 
    FROM MyDB.data AS d WHERE d.primary_key_column = NEW.primary_key_column; 

CREATE TRIGGER MyDB.data__bd BEFORE DELETE ON MyDB.data FOR EACH ROW 
    INSERT INTO MyDB.data_history SELECT 'delete', NULL, NOW(), d.* 
    FROM MyDB.data AS d WHERE d.primary_key_column = OLD.primary_key_column; 

大功告成現在,所有的刀片,更新和刪除'MyDb.dat一個」將被記錄在‘MyDb.data_history’,給你這樣一個歷史表(減去做作‘data_columns’列)

ID revision action data columns.. 
1  1   'insert' ....   initial entry for row where ID = 1 
1  2   'update' ....   changes made to row where ID = 1 
2  1   'insert' ....   initial entry, ID = 2 
3  1   'insert' ....   initial entry, ID = 3 
1  3   'update' ....   more changes made to row where ID = 1 
3  2   'update' ....   changes made to row where ID = 3 
2  2   'delete' ....   deletion of row where ID = 2 

要更新顯示給定列中的變化進行更新,您需要在主鍵和順序列上將自己的歷史表加入自己。你可以創建一個視圖用於此目的,例如:

CREATE VIEW data_history_changes AS 
    SELECT t2.dt_datetime, t2.action, t1.primary_key_column as 'row id', 
    IF(t1.a_column = t2.a_column, t1.a_column, CONCAT(t1.a_column, " to ", t2.a_column)) as a_column 
    FROM MyDB.data_history as t1 INNER join MyDB.data_history as t2 on t1.primary_key_column = t2.primary_key_column 
    WHERE (t1.revision = 1 AND t2.revision = 1) OR t2.revision = t1.revision+1 
    ORDER BY t1.primary_key_column ASC, t2.revision ASC 
+3

我非常喜歡這個解決方案。但是如果你的主表沒有主鍵,或者你不知道主鍵是什麼,它有點棘手。 – Umingo

+1

哇!這太棒了。完美無瑕的工作! –

+0

由於如何將原始表中的所有索引複製到歷史記錄表(由於CREATE TABLE ... LIKE ....的工作方式),我最近遇到了使用此解決方案的問題。在歷史記錄表上具有唯一索引可能會導致AFTER UPDATE觸發器中的INSERT查詢變爲barf,因此需要將其刪除。 在PHP腳本中,我有這樣做的東西,我查詢新創建的歷史表上的任何唯一索引(「SHOW INDEX FROM data_table WHERE Key_name!='PRIMARY'and Non_unique = 0」),然後刪除它們。 –

4

下面是我們如何解決它

一個用戶表看起來像這樣

Users 
------------------------------------------------- 
id | name | address | phone | email | created_on | updated_on 

和業務需求發生變化,我們需要檢查用戶以前的所有以前的地址和電話號碼。 新的模式是這樣的

Users (the data that won't change over time) 
------------- 
id | name 

UserData (the data that can change over time and needs to be tracked) 
------------------------------------------------- 
id | id_user | revision | city | address | phone | email | created_on 
1 | 1  | 0  | NY | lake st | 9809 | @long | 2015-10-24 10:24:20 
2 | 1  | 2  | Tokyo| lake st | 9809 | @long | 2015-10-24 10:24:20 
3 | 1  | 3  | Sdny | lake st | 9809 | @long | 2015-10-24 10:24:20 
4 | 2  | 0  | Ankr | lake st | 9809 | @long | 2015-10-24 10:24:20 
5 | 2  | 1  | Lond | lake st | 9809 | @long | 2015-10-24 10:24:20 

要找到任何用戶的當前地址,我們搜索的UserData與修訂DESC和LIMIT 1

要獲得的時間 一定時期之間的用戶的地址我們可以使用created_on bewteen(date1,date 2)

+0

這是我想要的解決方案,但我想知道 如何使用觸發器在此表中插入id_user ? –

+1

「id_user = 1」的'revision = 1'發生了什麼?首先,我認爲你的計數是'0,2,3,...',但後來我看到'id_user = 2'的修訂計數是'0,1,...' – Pathros

+0

你不需要'id'和'id_user'列'。只需使用「ID」(用戶ID)和「修訂」的組ID。 – Gajus

0

這樣做的直接方法是在表上創建觸發器。設置一些條件或映射方法。當更新或刪除發生時,它會自動插入「更改」表中。

但最大的部分是如果我們有很多列和大量的表。我們必須輸入每個表的每個列的名稱。顯然,這是浪費時間。

爲了更華麗地處理這個問題,我們可以創建一些程序或函數來檢索列的名稱。

我們也可以使用第三部分的工具來簡單地做到這一點。在這裏,我寫了一個Java程序 Mysql Tracker

+0

我如何使用你的Mysql Tracker? – webchun

+0

1.確保在每個表中有一個id列作爲主鍵。 2.將java文件複製到本地(或IDE) 3.根據您的數據庫配置和結構導入庫並編輯第9-15行的靜態變量。 4.解析並運行java文件 5.複製控制檯日誌並將其作爲Mysql命令執行 – goforu

2

只是我2美分。我會創建一個解決方案,記錄改變的內容,與瞬態解決方案非常相似。

我ChangesTable將簡單爲:

DateTime | WhoChanged | TableName | Action | ID |FieldName | OldValue

1)當整個行在主表發生變化,許多條目,將進入這個表,但是這是非常不可能的,所以不是大問題(人們通常只改變一件事) 2)OldVaue(和NewValue,如果你想要的話)必須是某種史詩般的「任何類型」,因爲它可以是任何數據,可能有辦法用RAW類型來做到這一點或者只是使用JSON字符串來轉換進出。

最小的數據使用率,存儲您需要的一切,並且可以一次用於所有表格。我現在正在自己研究這一點,但這可能最終會成爲我走的路。

對於創建和刪除,只需要行ID,不需要字段。在主表上刪除一個標誌(active?)會很好。

1

爲什麼不簡單地使用bin日誌文件?如果在Mysql服務器上設置複製,並且binlog文件格式設置爲ROW,則可以捕獲所有更改。

可以使用一個很好的名爲noplay的python庫。更多信息here

+0

即使您沒有/需要複製,也可以使用Binlog。 Binlog有許多有益的用例。如上所述,複製可能是最常見的用例,但它也可以用於備份和審計歷史記錄。 – webaholik