Автор оригинала: Vlad Mihalcea.
Вступление
В этой статье мы рассмотрим, как мы можем реализовать механизм ведения журнала аудита с использованием триггеров базы данных MySQL для хранения старых и новых состояний строк в типах столбцов JSON.
Таблицы базы данных
Давайте предположим, что у нас есть библиотечное приложение, содержащее следующие две таблицы:
В таблице книга хранятся все книги, найденные в нашей библиотеке, а в таблице book_audit_log хранятся CDC (Запись данных об изменении) события, произошедшие с данной книгой записью с помощью инструкции INSERT, UPDATE или DELETE DML.
Таблица book_audit_log создается следующим образом:
CREATE TABLE book_audit_log (
book_id BIGINT NOT NULL,
old_row_data JSON,
new_row_data JSON,
dml_type ENUM('INSERT', 'UPDATE', 'DELETE') NOT NULL,
dml_timestamp TIMESTAMP NOT NULL,
dml_created_by VARCHAR(255) NOT NULL,
PRIMARY KEY (book_id, dml_type, dml_timestamp)
)
В столбцах book_audit_log хранится следующая информация:
- В столбце
book_idхранится идентификатор строкиbook, которая была создана, обновлена или удалена. -
old_row_data– это столбец JSON, который будет фиксировать состояние записиbookперед выполнением инструкции INSERT, UPDATE или DELETE. -
new_row_data– это столбец JSON, который будет фиксировать состояние записиbookпосле выполнения инструкции INSERT, UPDATE или DELETE. -
dml_type– это столбец перечисления, в котором хранится тип инструкции DML, который создал, обновил или удалил даннуюкнигузапись. - В
dml_timestampхранится метка времени выполнения инструкции DML. -
dml_created_byхранит пользователя приложения, который выдал инструкцию INSERT, UPDATE или DELETE DML.
Первичный ключ book_audit_log является составным из book_id , dmi_type и dmi_timestamp , поскольку строка book может иметь несколько связанных book_audit_log записей.
Триггеры ведения журнала аудита MySQL
Чтобы записать инструкции INSERT, UPDATE и DELETE DML, нам нужно создать 3 триггера базы данных, которые будут вставлять записи в таблицу book_audit_log .
MySQL ПОСЛЕ триггера ВСТАВКИ
Чтобы перехватить инструкции INSERT в таблице book , мы создадим book_insert_audit_trigger :
CREATE TRIGGER book_insert_audit_trigger
AFTER INSERT ON book FOR EACH ROW
BEGIN
INSERT INTO book_audit_log (
book_id,
old_row_data,
new_row_data,
dml_type,
dml_timestamp,
dml_created_by
)
VALUES(
NEW.id,
null,
JSON_OBJECT(
"title", NEW.title,
"author", NEW.author,
"price_in_cents", NEW.price_in_cents,
"publisher", NEW.publisher
),
'INSERT',
CURRENT_TIMESTAMP,
@logged_user
);
END
book_insert_audit_trigger_ выполняется после вставки записи в таблицу book .
Ключевое слово NEW ссылается на только что вставленные значения записей, поэтому мы можем использовать его для извлечения значений столбцов book .
Установлен только столбец new_row_data , так как в столбце old_row_data нет состояния старой записи, которое можно установить.
Функция JSON_OBJECT MySQL позволяет нам создать объект JSON, который принимает предоставленные пары ключ-значение.
В столбце dml_type задано значение INSERT , а в столбце dmi_timestamp задано значение CURRENT_TIMESTAMP .
В столбце dml_created_by задано значение переменной сеанса @logged_user MySQL, которое ранее было задано приложением для текущего зарегистрированного пользователя:
Session session = entityManager.unwrap(Session.class);
Dialect dialect = session.getSessionFactory()
.unwrap(SessionFactoryImplementor.class)
.getJdbcServices()
.getDialect();
session.doWork(connection -> {
update(
connection,
String.format(
"SET @logged_user = '%s'",
ReflectionUtils.invokeMethod(
dialect,
"escapeLiteral",
LoggedUser.get()
)
)
);
});
MySQL ПОСЛЕ запуска ОБНОВЛЕНИЯ
Чтобы зафиксировать операторы ОБНОВЛЕНИЯ в записях book , мы создадим следующий book_update_audit_trigger_ |:
CREATE TRIGGER book_update_audit_trigger
AFTER UPDATE ON book FOR EACH ROW
BEGIN
INSERT INTO book_audit_log (
book_id,
old_row_data,
new_row_data,
dml_type,
dml_timestamp,
dml_created_by
)
VALUES(
NEW.id,
JSON_OBJECT(
"title", OLD.title,
"author", OLD.author,
"price_in_cents", OLD.price_in_cents,
"publisher", OLD.publisher
),
JSON_OBJECT(
"title", NEW.title,
"author", NEW.author,
"price_in_cents", NEW.price_in_cents,
"publisher", NEW.publisher
),
'UPDATE',
CURRENT_TIMESTAMP,
@logged_user
);
END
Каждый раз, когда книга запись обновляется, выполняется book_update_audit_trigger_ и будет создана строка book_audit_log для записи как старого, так и нового состояния изменяющейся книги записи.
MySQL ПОСЛЕ триггера УДАЛЕНИЯ
Чтобы перехватить операторы УДАЛЕНИЯ в строках таблицы book , мы создадим следующий book_delete_audit_trigger :
CREATE TRIGGER book_delete_audit_trigger
AFTER DELETE ON book FOR EACH ROW
BEGIN
INSERT INTO book_audit_log (
book_id,
old_row_data,
new_row_data,
dml_type,
dml_timestamp,
dml_created_by
)
VALUES(
OLD.id,
JSON_OBJECT(
"title", OLD.title,
"author", OLD.author,
"price_in_cents", OLD.price_in_cents,
"publisher", OLD.publisher
),
null,
'DELETE',
CURRENT_TIMESTAMP,
@logged_user
);
END
Как вы можете видеть, установлен только столбец old_row_data , поскольку состояние новой записи отсутствует.
Время тестирования
При выполнении инструкции INSERT в таблице книга :
INSERT INTO book (
id,
author,
price_in_cents,
publisher,
title
)
VALUES (
1,
'Vlad Mihalcea',
3990,
'Amazon',
'High-Performance Java Persistence 1st edition'
)
Мы видим, что в book_audit_log вставлена запись, которая фиксирует инструкцию INSERT, только что выполненную в таблице book :
| book_id | old_row_data | new_row_data | dml_type | dml_timestamp | dml_created_by |
|---------|--------------|--------------------------------------------------------------------------------------------------------------------------------------|----------|---------------------|----------------|
| 1 | | {"title": "High-Performance Java Persistence 1st edition", "author": "Vlad Mihalcea", "publisher": "Amazon", "price_in_cents": 3990} | INSERT | 2020-07-29 13:40:15 | Vlad Mihalcea |
При обновлении строки книга таблица:
UPDATE book SET price_in_cents = 4499 WHERE id = 1
Мы видим, что новая запись будет добавлена в book_audit_log триггером AFTER UPDATE в таблице book .:
| book_id | old_row_data | new_row_data | dml_type | dml_timestamp | dml_created_by |
|---------|--------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|----------|---------------------|----------------|
| 1 | | {"title": "High-Performance Java Persistence 1st edition", "author": "Vlad Mihalcea", "publisher": "Amazon", "price_in_cents": 3990} | INSERT | 2020-07-29 13:40:15 | Vlad Mihalcea |
| 1 | {"title": "High-Performance Java Persistence 1st edition", "author": "Vlad Mihalcea", "publisher": "Amazon", "price_in_cents": 3990} | {"title": "High-Performance Java Persistence 1st edition", "author": "Vlad Mihalcea", "publisher": "Amazon", "price_in_cents": 4499} | UPDATE | 2020-07-29 13:50:48 | Vlad Mihalcea |
При удалении строки книга таблица:
DELETE FROM book WHERE id = 1
Новая запись добавляется в book_audit_log триггером ПОСЛЕ УДАЛЕНИЯ в таблице book :
| book_id | old_row_data | new_row_data | dml_type | dml_timestamp | dml_created_by |
|---------|--------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|----------|---------------------|----------------|
| 1 | | {"title": "High-Performance Java Persistence 1st edition", "author": "Vlad Mihalcea", "publisher": "Amazon", "price_in_cents": 3990} | INSERT | 2020-07-29 13:40:15 | Vlad Mihalcea |
| 1 | {"title": "High-Performance Java Persistence 1st edition", "author": "Vlad Mihalcea", "publisher": "Amazon", "price_in_cents": 3990} | {"title": "High-Performance Java Persistence 1st edition", "author": "Vlad Mihalcea", "publisher": "Amazon", "price_in_cents": 4499} | UPDATE | 2020-07-29 13:50:48 | Vlad Mihalcea |
| 1 | {"title": "High-Performance Java Persistence 1st edition", "author": "Vlad Mihalcea", "publisher": "Amazon", "price_in_cents": 4499} | | DELETE | 2020-07-29 14:05:33 | Vlad Mihalcea |
Если вы хотите преобразовать объект журнала аудита на основе JSON в начальную запись реляционной таблицы, вы можете использовать функцию MySQL JSON_TABLE , как описано в этой статье .
Потрясающе, правда?
Вывод
Когда дело доходит до внедрения механизма ведения журнала аудита, существует множество вариантов на выбор. Если вы используете Hibernate, самое простое решение-использовать энверы Hibernate .
Если вы не используете режим гибернации или хотите фиксировать события CDC независимо от того, как генерируются операторы DML (например, обычный JDBC, с помощью консоли SQL), то решение для запуска базы данных очень легко реализовать. Использование столбцов JSON для хранения старого и нового состояния строки, которая создается, обновляется или удаляется, намного лучше, чем перечисление всех столбцов в таблице журнала аудита.
Другой вариант-использовать Debezium и извлекать события CDC из двоичного журнала. Это решение может работать асинхронно, поэтому не влияет на транзакции OLTP, которые запускают события CDC.