История изменения данных с таблицами аудита: группировка изменений

Допустим, я хочу хранить пользователей и группы в базе данных MySQL. У них есть отношение n: m. Для отслеживания всех изменений в каждой таблице есть таблица аудита user_journal, group_journal и user_group_journal. Триггеры MySQL копируют текущую запись в таблицу журнала при каждом INSERT или UPDATE (DELETES не поддерживаются, потому что мне нужна информация о том, какой пользователь приложения удалил запись, поэтому есть флаг active, который будет установлен на 0 вместо удаление).

Мой вопрос / проблема: если я добавляю в группу сразу 10 пользователей. Когда я позже просматриваю историю этой группы в пользовательском интерфейсе приложения, я хочу видеть добавление этих 10 пользователей как за один шаг, а не как за 10 независимых шагов. Есть ли хорошее решение для группировки таких изменений вместе? Может быть, можно иметь счетчик, который увеличивается каждый раз, когда триггер ... срабатывает? Никогда не работал с триггерами.

Лучшее решение - собрать воедино все изменения, внесенные в транзакцию. Поэтому, когда пользователь обновляет имя группы и добавляет 10 пользователей за один шаг (один вызов контроллера формы), это будет один шаг в истории. Может быть, можно определить случайный хеш или увеличить глобальный счетчик каждый раз при запуске транзакции и получить доступ к этому значению в триггере?

Я не хочу усложнять дизайн таблицы, чем иметь одну журнальную таблицу для каждой «реальной» таблицы. Я не хочу добавлять хеш транзакции в каждую таблицу базы данных (имеется в виду «настоящие» таблицы, а не таблицы аудита - там, конечно, все было бы хорошо). Также хотелось бы иметь решение в базе данных, а не в приложении.


person stofl    schedule 01.01.2013    source источник


Ответы (2)


Я немного поигрался и нашел очень хорошее решение:

Настройка базы данных

# 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 в таблицы аудита, чтобы иметь возможность записывать удаления.

person stofl    schedule 03.02.2013

Предполагая, что скорость добавления группы пользователей в группу составляет менее одного раза в секунду ....

Я бы предложил просто добавить столбец типа timestamp с именем типа added_timestamp к user_group и user_group_journal. НЕ СДЕЛАЙТЕ ЭТО АВТОМАТИЧЕСКИЙ МОДУЛЬ ОБНОВЛЕНИЯ ИЛИ ПО УМОЛЧАНИЮ CURRENT_TIMESTAMP, вместо этого в коде, когда вы вставляете пакетно в user_group, вычислите текущую дату и время, а затем вручную установите это для всех новых запись user_group.

Возможно, вам придется изменить настройку, чтобы добавить поле, которое будет скопировано остальной частью новой user_group записи в user_group_journal таблицу.

Затем, когда вы можете создать запрос / представление, которое группируется по group_id и новому столбцу added_timestamp.

Если требуется большая точность, то через 1 секунду вы можете использовать строковый столбец и заполнить его строковым представлением более детального времени (которое вам нужно будет сгенерировать, однако библиотеки, которые позволяет использовать ваш язык).

person Ray    schedule 01.01.2013
comment
Хорошее, простое решение. - person ToolmakerSteve; 25.03.2016