【问题标题】:Data change history with audit tables: Grouping changes带有审计表的数据更改历史记录:分组更改
【发布时间】:2024-01-02 06:10:01
【问题描述】:

假设我想将用户和组存储在 MySQL 数据库中。他们有一个关系n:m。为了跟踪所有更改,每个表都有一个审计表 user_journal、group_journal 和 user_group_journal。 MySQL 触发器在每次 INSERT 或 UPDATE 时将当前记录复制到日志表(不支持 DELETES,因为我需要应用程序用户删除记录的信息——所以有一个标志 active 将设置为 @ 987654322@ 而不是删除)。

我的问题/问题是:假设我一次将 10 个用户添加到一个组中。当我稍后在应用程序的用户界面中单击该组的历史记录时,我希望将这 10 个用户的添加视为 一步 而不是 10 个独立的步骤。 有没有很好的解决方案将这些更改组合在一起?也许可以有一个计数器,每次触发器被触发时都会增加一个计数器?我从未使用过触发器。

最好的解决方案是将事务中所做的所有更改放在一起。因此,当用户更新组的名称并一步添加 10 个用户(一个表单控制器调用)时,这将是历史记录中的一步。也许可以在每次启动事务时定义一个随机哈希或增加一个全局计数器并在触发器中访问该值?

我不想让表格设计比为每个“真实”表格设置一个日记帐表格更复杂。我不想在每个数据库表中添加事务哈希(意思是“真实”表,而不是审计表——当然可以)。我还想在数据库中找到解决方案,而不是在应用程序中。

【问题讨论】:

    标签: mysql triggers audit-tables


    【解决方案1】:

    我玩了一会儿,现在我找到了一个很好的解决方案:

    数据库设置

    # First of all I create the database and the basic table:
    
    DROP DATABASE `mytest`;
    CREATE DATABASE `mytest`;
    USE `mytest`;
    CREATE TABLE `test` (
        `id` INT PRIMARY KEY AUTO_INCREMENT,
        `something` VARCHAR(255) NOT NULL
    );
    
    # Then I add an audit table to the database:
    
    CREATE TABLE `audit_trail_test` (
        `_id` INT PRIMARY KEY AUTO_INCREMENT,
        `_revision_id` VARCHAR(255) NOT NULL,
        `id` INT NOT NULL,
        `something` VARCHAR(255) NOT NULL
    );
    
    # I added a field _revision_id to it. This is 
    # the ID that groups together all changes a
    # user made within a request of that web
    # application (written in PHP). So we need a
    # third table to store the time and the user
    # that made the changes of that revision:
    
    CREATE TABLE `audit_trail_revisions` (
        `id` INT PRIMARY KEY AUTO_INCREMENT,
        `user_id` INT NOT NULL,
        `time` DATETIME NOT NULL
    );
    
    # Now we need a procedure that creates a
    # record in the revisions table each time an
    # insert or update trigger will be called.
    
    DELIMITER $$
    
    CREATE PROCEDURE create_revision_record()
    BEGIN
        IF @revision_id IS NULL THEN
            INSERT INTO `audit_trail_revisions`
                (user_id, `time`)
                    VALUES
                (@user_id, @time);
            SET @revision_id = LAST_INSERT_ID();
        END IF;
    END;
    
    # It checks if a user defined variable
    # @revision_id is set and if not it creates
    # the row and stores the generated ID (auto
    # increment) into that variable.
    # 
    # Next I wrote the two triggers:
    
    CREATE TRIGGER `test_insert` AFTER INSERT ON `test` 
        FOR EACH ROW BEGIN
            CALL create_revision_record();
            INSERT INTO `audit_trail_test`
                (
                    id,
                    something,
                    _revision_id
                ) 
            VALUES
                (
                    NEW.id,
                    NEW.something,
                    @revision_id
                );
        END;
    $$
    
    CREATE TRIGGER `test_update` AFTER UPDATE ON `test` 
        FOR EACH ROW BEGIN
            CALL create_revision_record();
            INSERT INTO `audit_trail_test`
                (
                    id,
                    something,
                    _revision_id
                ) 
            VALUES
                (
                    NEW.id,
                    NEW.something,
                    @revision_id
                );
        END;
    $$
    

    应用程序代码 (PHP)

    $iUserId = 42;
    
    $Database = new \mysqli('localhost', 'root', 'root', 'mytest');
    
    if (!$Database->query('SET @user_id = ' . $iUserId . ', @time = NOW()'))
        die($Database->error);
    if (!$Database->query('INSERT INTO `test` VALUES (NULL, "foo")'))
        die($Database->error);
    if (!$Database->query('UPDATE `test` SET `something` = "bar"'))
        die($Database->error);
    
    // To simulate a second request we close the connection,
    // sleep 2 seconds and create a second connection.
    $Database->close();
    sleep(2);
    $Database = new \mysqli('localhost', 'root', 'root', 'mytest');
    
    if (!$Database->query('SET @user_id = ' . $iUserId . ', @time = NOW()'))
        die($Database->error);
    if (!$Database->query('UPDATE `test` SET `something` = "baz"'))
        die($Database->error);
    

    然后……结果

    mysql> select * from test;
    +----+-----------+
    | id | something |
    +----+-----------+
    |  1 | baz       |
    +----+-----------+
    1 row in set (0.00 sec)
    
    mysql> select * from audit_trail_test;
    +-----+--------------+----+-----------+
    | _id | _revision_id | id | something |
    +-----+--------------+----+-----------+
    |   1 | 1            |  1 | foo       |
    |   2 | 1            |  1 | bar       |
    |   3 | 2            |  1 | baz       |
    +-----+--------------+----+-----------+
    3 rows in set (0.00 sec)
    
    mysql> select * from audit_trail_revisions;
    +----+---------+---------------------+
    | id | user_id | time                |
    +----+---------+---------------------+
    |  1 |      42 | 2013-02-03 17:13:20 |
    |  2 |      42 | 2013-02-03 17:13:22 |
    +----+---------+---------------------+
    2 rows in set (0.00 sec)
    

    如果我遗漏了一点,请告诉我。我必须在审计表中添加一个action 列才能记录删除。

    【讨论】:

      【解决方案2】:

      假设您将一批用户添加到一个组的速率低于每秒​​一次......

      我建议在user_groupuser_group_journal 中简单地添加一个名为added_timestamp 的时间戳类型的列。 不要将其设为自动更新时间戳或将其默认设置为 CURRENT_TIMESTAMP,而是在您的代码中,当您批量插入 user_group 时,计算当前日期和时间,然后为所有新的用户组记录。

      您可能需要调整设置以将要复制的字段添加到 user_group 新记录的其余部分到 user_group_journal 表中。

      然后,当您可以创建分组在 group_id 和新的 added_timestamp 列上的查询/视图时。

      如果需要更高的保真度,然后 1 秒,您可以使用字符串列并使用更精细时间的字符串表示填充它(您需要生成您使用的语言允许的库)。

      【讨论】:

      • 好,简单的解决方案。