2012-09-24 342 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