如何在 MySQL 中使用触发器创建审计跟踪或日志记录表
How to create audit trail or logging tables with triggers in MySQL
我想要一个在外借 table 更新(即归还书籍)时触发的触发器。它应该仅在贷款逾期时从贷款 table 中的行中获取值,并将它们插入新的 table.
'loan' table:
CREATE TABLE loan (
book_code INT NOT NULL,
student_num INT NOT NULL,
out_date DATE NOT NULL,
due_date DATE NOT NULL,
return_date DATE,
CONSTRAINT pk_loan PRIMARY KEY (book_code, student_num, out_date),
CONSTRAINT fk_book_code FOREIGN KEY (book_code) REFERENCES copy(book_code),
CONSTRAINT fk_num FOREIGN KEY (student_num) REFERENCES student(student_num)
);
和 'overdue' table
CREATE TABLE overdue (
overdue_id INT NOT NULL AUTO_INCREMENT,
student_num INT NOT NULL,
out_date DATE NOT NULL,
due_date DATE NOT NULL,
return_date DATE,
CONSTRAINT pk_overdue PRIMARY KEY (overdue_id),
CONSTRAINT fk_num FOREIGN KEY (student_num) REFERENCES student(student_num)
);
到目前为止我得到了什么:
DELIMITER $$
CREATE TRIGGER trg_overdue_loans AFTER UPDATE ON loan FOR EACH ROW
BEGIN
IF (NEW.return_date > OLD.due_date) THEN
INSERT INTO overdue (student_num, out_date, due_date, return_date)
VALUES (OLD.student_num, OLD.out_date, OLD.due_date, NEW.return_date)
END IF;
END$$
DELIMITER ;
我在 END IF
上得到 "an error in (my) SQL syntax",我不知道为什么。任何帮助将不胜感激!
试试这个,你的语法和分隔符中缺少分号
DROP TRIGGER IF EXISTS trg_overdue_loans;
DELIMITER $$
CREATE TRIGGER `trg_overdue_loans` AFTER UPDATE ON loan FOR EACH ROW
BEGIN
IF NEW.return_date > OLD.due_date THEN
INSERT INTO overdue (student_num, out_date, due_date, return_date)
VALUES (OLD.student_num, OLD.out_date, OLD.due_date, NEW.return_date);
END IF;
END;$$
DELIMITER ;
我创建了一个名为 cdc_audit 的工具,它可以在 mysql 中为任何或所有 table 自动创建审计 table,甚至可以保留预先存在的触发器.也许您或其他人会发现它有用
特点
- 自动生成审计 tables
- 自动生成触发器以填充审计 tables
- 自动将审计 table 中的新行同步到 .csv 文件。
- 读取 mysql information_schema 以自动确定 table 和列。
- 可以为所有数据库 table 或指定列表生成 tables + 触发器。
- 可以为所有数据库 table 或指定列表同步审核 table。
- 在生成 AFTER 触发器时保留预先存在的触发器逻辑(如果有)。
- 用于删除除最后一个审计行之外的所有内容的同步脚本选项,以保持源数据库较小。
更新:这是一个示例,在名为 Whosebug 的测试数据库中使用上面的贷款 table。
$ ./cdc_audit_gen_mysql.php -t loan -d Whosebug
Successfully Generated Audit Tables + Triggers in ./cdc_audit_gen
现在让我们 运行 sql 在数据库中创建审计 table 和触发器。
$ mysql -u root Whosebug < cdc_audit_gen/loan.audit.sql
就是这样。审核 table 加上触发器已到位。
如果好奇,我们可以检查实现。
$ cat cdc_audit_gen/loan.audit.sql
/**
* Audit table for table (loan).
*
* !!! DO NOT MODIFY THIS FILE MANUALLY !!!
*
* This file is auto-generated and is NOT intended
* for manual modifications/extensions.
*
* For additional documentation, see:
* https://github.com/dan-da/cdc_audit
*
*/
create table if not exists `loan_audit` (
`book_code` int(11) not null comment 'Primary key in source table loan',
`student_num` int(11) not null comment 'Primary key in source table loan',
`out_date` date not null comment 'Primary key in source table loan',
`due_date` date not null comment '',
`return_date` date null comment '',
`audit_event` enum('insert','update','delete') not null comment 'Indicates event that occurred in source table',
`audit_timestamp` timestamp not null comment 'Updated when record is inserted, updated or deleted in source table',
`audit_pk` int(11) not null primary key auto_increment comment 'Audit table primary key, useful for sorting since mysql time data types are only granular to second level.',
index (`book_code`, `student_num`, `out_date`),
index (`audit_timestamp`)
);
/**
* Audit triggers for table (loan).
*
* For additional documentation, see:
* https://github.com/dan-da/cdc_audit
*
*/
-- loan after INSERT trigger.
DELIMITER @@
CREATE TRIGGER `loan_after_insert` AFTER INSERT ON `loan`
FOR EACH ROW BEGIN
insert into `loan_audit` (`book_code`, `student_num`, `out_date`, `due_date`, `return_date`, `audit_event`, `audit_timestamp`) values(NEW.`book_code`, NEW.`student_num`, NEW.`out_date`, NEW.`due_date`, NEW.`return_date`, 'insert', CURRENT_TIMESTAMP);
END;
@@
-- loan after UPDATE trigger.
DELIMITER @@
CREATE TRIGGER `loan_after_update` AFTER UPDATE ON `loan`
FOR EACH ROW BEGIN
insert into `loan_audit` (`book_code`, `student_num`, `out_date`, `due_date`, `return_date`, `audit_event`, `audit_timestamp`) values(NEW.`book_code`, NEW.`student_num`, NEW.`out_date`, NEW.`due_date`, NEW.`return_date`, 'update', CURRENT_TIMESTAMP);
END;
@@
-- loan after DELETE trigger.
DELIMITER @@
CREATE TRIGGER `loan_after_delete` AFTER DELETE ON `loan`
FOR EACH ROW BEGIN
insert into `loan_audit` (`book_code`, `student_num`, `out_date`, `due_date`, `return_date`, `audit_event`, `audit_timestamp`) values(OLD.`book_code`, OLD.`student_num`, OLD.`out_date`, OLD.`due_date`, OLD.`return_date`, 'delete', CURRENT_TIMESTAMP);
END;
我明白我来不及参加聚会了...不过我正在使用的以下代码将有助于在任何 table 名称的任何数据库上创建审计试验。
SET GLOBAL group_concat_max_len = 1000;
SET @dbName = "sample_schema_name";
SET @tableName = "sample_table_name";
SELECT concat("DROP TABLE IF EXISTS `", @dbName, "`.`", table_data.audit_table, "`;\r",
"CREATE TABLE `", @dbName, "`.`", table_data.audit_table, "`\r",
"(\r",
" `auditAction` ENUM ('INSERT', 'UPDATE', 'DELETE'),\r",
" `auditTimestamp` timestamp DEFAULT CURRENT_TIMESTAMP,\r",
" `auditId` INT(14) AUTO_INCREMENT,",
column_defs, ",\r"
" PRIMARY KEY (`auditId`),\r",
" INDEX (`auditTimestamp`)\r",
")\r",
" ENGINE = InnoDB;\r\r",
"DROP TRIGGER IF EXISTS `", @dbName, "`.`", table_data.insert_trigger, "`;\r",
"CREATE TRIGGER `", @dbName, "`.`", table_data.insert_trigger, "`\r",
" AFTER INSERT ON `", @dbName, "`.`", table_data.db_table, "`\r",
" FOR EACH ROW INSERT INTO `", @dbName, "`.`", table_data.audit_table, "`\r",
" (`auditAction`,", table_data.column_names, ")\r",
" VALUES\r",
" ('INSERT',", table_data.NEWcolumn_names, ");\r\r",
"DROP TRIGGER IF EXISTS `", @dbName, "`.`", table_data.update_trigger, "`;\r",
"CREATE TRIGGER `", @dbName, "`.`", table_data.update_trigger, "`\r",
" AFTER UPDATE ON `", @dbName, "`.`", table_data.db_table, "`\r",
" FOR EACH ROW INSERT INTO `", @dbName, "`.`", table_data.audit_table, "`\r",
" (`auditAction`,", table_data.column_names, ")\r",
" VALUES\r",
" ('UPDATE',", table_data.NEWcolumn_names, ");\r\r",
"DROP TRIGGER IF EXISTS `", @dbName, "`.`", table_data.delete_trigger, "`;\r",
"CREATE TRIGGER `", @dbName, "`.`", table_data.delete_trigger, "`\r",
" AFTER DELETE ON `", @dbName, "`.`", table_data.db_table, "`\r",
" FOR EACH ROW INSERT INTO `", @dbName, "`.`", table_data.audit_table, "`\r",
" (`auditAction`,", table_data.column_names, ")\r",
" VALUES\r",
" ('DELETE',", table_data.OLDcolumn_names, ");\r\r"
)
FROM (
# This select builds a derived table of table names with ordered and grouped column information in different
# formats as needed for audit table definitions and trigger definitions.
SELECT
table_order_key,
table_name AS db_table,
concat("audit_", table_name) AS audit_table,
concat(table_name, "_inserts") AS insert_trigger,
concat(table_name, "_updates") AS update_trigger,
concat(table_name, "_deletes") AS delete_trigger,
group_concat("\r `", column_name, "` ", column_type ORDER BY column_order_key) AS column_defs,
group_concat("`", column_name, "`" ORDER BY column_order_key) AS column_names,
group_concat("`NEW.", column_name, "`" ORDER BY column_order_key) AS NEWcolumn_names,
group_concat("`OLD.", column_name, "`" ORDER BY column_order_key) AS OLDcolumn_names
FROM
(
# This select builds a derived table of table names, column names and column types for
# non-audit tables of the specified db, along with ordering keys for later order by.
# The ordering must be done outside this select, as tables (including derived tables)
# are by definition unordered.
# We're only ordering so that the generated audit schema maintains a resemblance to the
# main schema.
SELECT
information_schema.tables.table_name AS table_name,
information_schema.columns.column_name AS column_name,
information_schema.columns.column_type AS column_type,
information_schema.tables.create_time AS table_order_key,
information_schema.columns.ordinal_position AS column_order_key
FROM information_schema.tables
JOIN information_schema.columns
ON information_schema.tables.table_name = information_schema.columns.table_name
WHERE information_schema.tables.table_schema = @dbName
AND information_schema.columns.table_schema = @dbName
AND information_schema.tables.table_name NOT LIKE "audit\_%"
) table_column_ordering_info
where table_name = @tableName
GROUP BY table_name
) table_data
ORDER BY table_order_key
在JSON
中存储新旧行状态
存储新旧行状态的最佳方法是使用 JSON 列。因此,对于每个要启用审核日志记录的 table,您可以创建一个审核日志 table,如下所示:
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_id
列存储已创建、更新或删除的 book
行的标识符。
old_row_data
是一个 JSON 列,它将在执行 INSERT、UPDATE 或 DELETE 语句之前捕获 book
记录的状态。
new_row_data
是一个 JSON 列,它将在执行 INSERT、UPDATE 或 DELETE 语句后捕获 book
记录的状态。
dml_type
是一个枚举列,用于存储创建、更新或删除给定 book
记录的 DML 语句类型。
dml_timestamp
存储DML语句执行时间戳。
dml_created_by
存储发出 INSERT、UPDATE 或 DELETE DML 语句的应用程序用户。
使用触发器拦截 INSERT、UPDATE 和 DELETE DML 语句
现在,要提供审核日志 tables,您需要创建以下 3 个触发器:
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
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
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
The JSON_OBJECT
MySQL function allows us to create a JSON object that takes the provided key-value pairs.
dml_type
列设置为 INSERT
、UPDATE
或 DELETE
的值并且 dml_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()
)
)
);
});
测试时间
在 book
table 上执行 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
中插入了一条记录,该记录捕获了刚刚在 book
table:
上执行的 INSERT 语句
| 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 |
更新 book
table 行时:
UPDATE book
SET price_in_cents = 4499
WHERE id = 1
我们可以看到 book
table:
上的 AFTER UPDATE 触发器将向 book_audit_log
添加一条新记录
| 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 |
删除 book
table 行时:
DELETE FROM book
WHERE id = 1
book
上的 AFTER DELETE 触发器将新记录添加到 book_audit_log
table:
| 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 |
就是这样!
我想要一个在外借 table 更新(即归还书籍)时触发的触发器。它应该仅在贷款逾期时从贷款 table 中的行中获取值,并将它们插入新的 table.
'loan' table:
CREATE TABLE loan (
book_code INT NOT NULL,
student_num INT NOT NULL,
out_date DATE NOT NULL,
due_date DATE NOT NULL,
return_date DATE,
CONSTRAINT pk_loan PRIMARY KEY (book_code, student_num, out_date),
CONSTRAINT fk_book_code FOREIGN KEY (book_code) REFERENCES copy(book_code),
CONSTRAINT fk_num FOREIGN KEY (student_num) REFERENCES student(student_num)
);
和 'overdue' table
CREATE TABLE overdue (
overdue_id INT NOT NULL AUTO_INCREMENT,
student_num INT NOT NULL,
out_date DATE NOT NULL,
due_date DATE NOT NULL,
return_date DATE,
CONSTRAINT pk_overdue PRIMARY KEY (overdue_id),
CONSTRAINT fk_num FOREIGN KEY (student_num) REFERENCES student(student_num)
);
到目前为止我得到了什么:
DELIMITER $$
CREATE TRIGGER trg_overdue_loans AFTER UPDATE ON loan FOR EACH ROW
BEGIN
IF (NEW.return_date > OLD.due_date) THEN
INSERT INTO overdue (student_num, out_date, due_date, return_date)
VALUES (OLD.student_num, OLD.out_date, OLD.due_date, NEW.return_date)
END IF;
END$$
DELIMITER ;
我在 END IF
上得到 "an error in (my) SQL syntax",我不知道为什么。任何帮助将不胜感激!
试试这个,你的语法和分隔符中缺少分号
DROP TRIGGER IF EXISTS trg_overdue_loans;
DELIMITER $$
CREATE TRIGGER `trg_overdue_loans` AFTER UPDATE ON loan FOR EACH ROW
BEGIN
IF NEW.return_date > OLD.due_date THEN
INSERT INTO overdue (student_num, out_date, due_date, return_date)
VALUES (OLD.student_num, OLD.out_date, OLD.due_date, NEW.return_date);
END IF;
END;$$
DELIMITER ;
我创建了一个名为 cdc_audit 的工具,它可以在 mysql 中为任何或所有 table 自动创建审计 table,甚至可以保留预先存在的触发器.也许您或其他人会发现它有用
特点
- 自动生成审计 tables
- 自动生成触发器以填充审计 tables
- 自动将审计 table 中的新行同步到 .csv 文件。
- 读取 mysql information_schema 以自动确定 table 和列。
- 可以为所有数据库 table 或指定列表生成 tables + 触发器。
- 可以为所有数据库 table 或指定列表同步审核 table。
- 在生成 AFTER 触发器时保留预先存在的触发器逻辑(如果有)。
- 用于删除除最后一个审计行之外的所有内容的同步脚本选项,以保持源数据库较小。
更新:这是一个示例,在名为 Whosebug 的测试数据库中使用上面的贷款 table。
$ ./cdc_audit_gen_mysql.php -t loan -d Whosebug
Successfully Generated Audit Tables + Triggers in ./cdc_audit_gen
现在让我们 运行 sql 在数据库中创建审计 table 和触发器。
$ mysql -u root Whosebug < cdc_audit_gen/loan.audit.sql
就是这样。审核 table 加上触发器已到位。
如果好奇,我们可以检查实现。
$ cat cdc_audit_gen/loan.audit.sql
/**
* Audit table for table (loan).
*
* !!! DO NOT MODIFY THIS FILE MANUALLY !!!
*
* This file is auto-generated and is NOT intended
* for manual modifications/extensions.
*
* For additional documentation, see:
* https://github.com/dan-da/cdc_audit
*
*/
create table if not exists `loan_audit` (
`book_code` int(11) not null comment 'Primary key in source table loan',
`student_num` int(11) not null comment 'Primary key in source table loan',
`out_date` date not null comment 'Primary key in source table loan',
`due_date` date not null comment '',
`return_date` date null comment '',
`audit_event` enum('insert','update','delete') not null comment 'Indicates event that occurred in source table',
`audit_timestamp` timestamp not null comment 'Updated when record is inserted, updated or deleted in source table',
`audit_pk` int(11) not null primary key auto_increment comment 'Audit table primary key, useful for sorting since mysql time data types are only granular to second level.',
index (`book_code`, `student_num`, `out_date`),
index (`audit_timestamp`)
);
/**
* Audit triggers for table (loan).
*
* For additional documentation, see:
* https://github.com/dan-da/cdc_audit
*
*/
-- loan after INSERT trigger.
DELIMITER @@
CREATE TRIGGER `loan_after_insert` AFTER INSERT ON `loan`
FOR EACH ROW BEGIN
insert into `loan_audit` (`book_code`, `student_num`, `out_date`, `due_date`, `return_date`, `audit_event`, `audit_timestamp`) values(NEW.`book_code`, NEW.`student_num`, NEW.`out_date`, NEW.`due_date`, NEW.`return_date`, 'insert', CURRENT_TIMESTAMP);
END;
@@
-- loan after UPDATE trigger.
DELIMITER @@
CREATE TRIGGER `loan_after_update` AFTER UPDATE ON `loan`
FOR EACH ROW BEGIN
insert into `loan_audit` (`book_code`, `student_num`, `out_date`, `due_date`, `return_date`, `audit_event`, `audit_timestamp`) values(NEW.`book_code`, NEW.`student_num`, NEW.`out_date`, NEW.`due_date`, NEW.`return_date`, 'update', CURRENT_TIMESTAMP);
END;
@@
-- loan after DELETE trigger.
DELIMITER @@
CREATE TRIGGER `loan_after_delete` AFTER DELETE ON `loan`
FOR EACH ROW BEGIN
insert into `loan_audit` (`book_code`, `student_num`, `out_date`, `due_date`, `return_date`, `audit_event`, `audit_timestamp`) values(OLD.`book_code`, OLD.`student_num`, OLD.`out_date`, OLD.`due_date`, OLD.`return_date`, 'delete', CURRENT_TIMESTAMP);
END;
我明白我来不及参加聚会了...不过我正在使用的以下代码将有助于在任何 table 名称的任何数据库上创建审计试验。
SET GLOBAL group_concat_max_len = 1000;
SET @dbName = "sample_schema_name";
SET @tableName = "sample_table_name";
SELECT concat("DROP TABLE IF EXISTS `", @dbName, "`.`", table_data.audit_table, "`;\r",
"CREATE TABLE `", @dbName, "`.`", table_data.audit_table, "`\r",
"(\r",
" `auditAction` ENUM ('INSERT', 'UPDATE', 'DELETE'),\r",
" `auditTimestamp` timestamp DEFAULT CURRENT_TIMESTAMP,\r",
" `auditId` INT(14) AUTO_INCREMENT,",
column_defs, ",\r"
" PRIMARY KEY (`auditId`),\r",
" INDEX (`auditTimestamp`)\r",
")\r",
" ENGINE = InnoDB;\r\r",
"DROP TRIGGER IF EXISTS `", @dbName, "`.`", table_data.insert_trigger, "`;\r",
"CREATE TRIGGER `", @dbName, "`.`", table_data.insert_trigger, "`\r",
" AFTER INSERT ON `", @dbName, "`.`", table_data.db_table, "`\r",
" FOR EACH ROW INSERT INTO `", @dbName, "`.`", table_data.audit_table, "`\r",
" (`auditAction`,", table_data.column_names, ")\r",
" VALUES\r",
" ('INSERT',", table_data.NEWcolumn_names, ");\r\r",
"DROP TRIGGER IF EXISTS `", @dbName, "`.`", table_data.update_trigger, "`;\r",
"CREATE TRIGGER `", @dbName, "`.`", table_data.update_trigger, "`\r",
" AFTER UPDATE ON `", @dbName, "`.`", table_data.db_table, "`\r",
" FOR EACH ROW INSERT INTO `", @dbName, "`.`", table_data.audit_table, "`\r",
" (`auditAction`,", table_data.column_names, ")\r",
" VALUES\r",
" ('UPDATE',", table_data.NEWcolumn_names, ");\r\r",
"DROP TRIGGER IF EXISTS `", @dbName, "`.`", table_data.delete_trigger, "`;\r",
"CREATE TRIGGER `", @dbName, "`.`", table_data.delete_trigger, "`\r",
" AFTER DELETE ON `", @dbName, "`.`", table_data.db_table, "`\r",
" FOR EACH ROW INSERT INTO `", @dbName, "`.`", table_data.audit_table, "`\r",
" (`auditAction`,", table_data.column_names, ")\r",
" VALUES\r",
" ('DELETE',", table_data.OLDcolumn_names, ");\r\r"
)
FROM (
# This select builds a derived table of table names with ordered and grouped column information in different
# formats as needed for audit table definitions and trigger definitions.
SELECT
table_order_key,
table_name AS db_table,
concat("audit_", table_name) AS audit_table,
concat(table_name, "_inserts") AS insert_trigger,
concat(table_name, "_updates") AS update_trigger,
concat(table_name, "_deletes") AS delete_trigger,
group_concat("\r `", column_name, "` ", column_type ORDER BY column_order_key) AS column_defs,
group_concat("`", column_name, "`" ORDER BY column_order_key) AS column_names,
group_concat("`NEW.", column_name, "`" ORDER BY column_order_key) AS NEWcolumn_names,
group_concat("`OLD.", column_name, "`" ORDER BY column_order_key) AS OLDcolumn_names
FROM
(
# This select builds a derived table of table names, column names and column types for
# non-audit tables of the specified db, along with ordering keys for later order by.
# The ordering must be done outside this select, as tables (including derived tables)
# are by definition unordered.
# We're only ordering so that the generated audit schema maintains a resemblance to the
# main schema.
SELECT
information_schema.tables.table_name AS table_name,
information_schema.columns.column_name AS column_name,
information_schema.columns.column_type AS column_type,
information_schema.tables.create_time AS table_order_key,
information_schema.columns.ordinal_position AS column_order_key
FROM information_schema.tables
JOIN information_schema.columns
ON information_schema.tables.table_name = information_schema.columns.table_name
WHERE information_schema.tables.table_schema = @dbName
AND information_schema.columns.table_schema = @dbName
AND information_schema.tables.table_name NOT LIKE "audit\_%"
) table_column_ordering_info
where table_name = @tableName
GROUP BY table_name
) table_data
ORDER BY table_order_key
在JSON
中存储新旧行状态存储新旧行状态的最佳方法是使用 JSON 列。因此,对于每个要启用审核日志记录的 table,您可以创建一个审核日志 table,如下所示:
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_id
列存储已创建、更新或删除的book
行的标识符。old_row_data
是一个 JSON 列,它将在执行 INSERT、UPDATE 或 DELETE 语句之前捕获book
记录的状态。new_row_data
是一个 JSON 列,它将在执行 INSERT、UPDATE 或 DELETE 语句后捕获book
记录的状态。dml_type
是一个枚举列,用于存储创建、更新或删除给定book
记录的 DML 语句类型。dml_timestamp
存储DML语句执行时间戳。dml_created_by
存储发出 INSERT、UPDATE 或 DELETE DML 语句的应用程序用户。
使用触发器拦截 INSERT、UPDATE 和 DELETE DML 语句
现在,要提供审核日志 tables,您需要创建以下 3 个触发器:
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
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
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
The
JSON_OBJECT
MySQL function allows us to create a JSON object that takes the provided key-value pairs.
dml_type
列设置为 INSERT
、UPDATE
或 DELETE
的值并且 dml_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()
)
)
);
});
测试时间
在 book
table 上执行 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
中插入了一条记录,该记录捕获了刚刚在 book
table:
| 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 |
更新 book
table 行时:
UPDATE book
SET price_in_cents = 4499
WHERE id = 1
我们可以看到 book
table:
book_audit_log
添加一条新记录
| 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 |
删除 book
table 行时:
DELETE FROM book
WHERE id = 1
book
上的 AFTER DELETE 触发器将新记录添加到 book_audit_log
table:
| 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 |
就是这样!