如何在 MySQL 中使用触发器创建审计跟踪或日志记录表

Din*_*ter 3 mysql sql t-sql triggers audit-trail

我想要一个触发器,每当更新贷款表时触发(即返回一本书)。它应该只从贷款表中的行中获取贷款逾期的值并将它们插入到新表中。


“贷款”表:

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)
);
Run Code Online (Sandbox Code Playgroud)

和“过期”表

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)
 );
Run Code Online (Sandbox Code Playgroud)

到目前为止我所得到的:

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 ;
Run Code Online (Sandbox Code Playgroud)

我在 上收到“(我的)SQL 语法错误” END IF,我不知道为什么。任何帮助都感激不尽!

Vla*_*cea 9

在 JSON 中存储新旧行状态

存储新旧行状态的最佳方法是使用 JSON 列。因此,对于要启用审核日志记录的每个表,您可以创建一个审核日志表,如下所示:

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)
)
Run Code Online (Sandbox Code Playgroud)
  • 该列存储已创建、更新或删除的book_id行的标识符。book
  • 这是一个 JSON 列,它将在执行 INSERT、UPDATE 或 DELETE 语句之前old_row_data捕获记录的状态。book
  • 这是一个 JSON 列,它将在执行 INSERT、UPDATE 或 DELETE 语句后new_row_data捕获记录的状态。book
  • dml_type一个枚举列,存储创建、更新或删除给定记录的 DML 语句类型book
  • 存储dml_timestampDML 语句执行时间戳。
  • 存储dml_created_by发出 INSERT、UPDATE 或 DELETE DML 语句的应用程序用户。

使用触发器拦截 INSERT、UPDATE 和 DELETE DML 语句

现在,为了提供审核日志表,您需要创建以下 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
Run Code Online (Sandbox Code Playgroud)

MySQLJSON_OBJECT函数允许我们创建一个采用提供的键值对的 JSON 对象。

dml_type列设置为 的值INSERTUPDATE或者DELETEdml_timestamp值设置为CURRENT_TIMESTAMP

dml_created_by列设置为@logged_userMySQL 会话变量的值,该变量先前由当前登录用户的应用程序设置:

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()
            )
        )
    );
});
Run Code Online (Sandbox Code Playgroud)

测试时间

在表上执行 INSERT 语句时book

INSERT INTO book (
    id,
    author, 
    price_in_cents, 
    publisher, 
    title
) 
VALUES (
    1,
    'Vlad Mihalcea', 
    3990, 
    'Amazon', 
    'High-Performance Java Persistence 1st edition'
)
Run Code Online (Sandbox Code Playgroud)

我们可以看到一条记录被插入到book_audit_log捕获刚刚在book表上执行的 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  |
Run Code Online (Sandbox Code Playgroud)

更新book表行时:

UPDATE book 
SET price_in_cents = 4499 
WHERE id = 1
Run Code Online (Sandbox Code Playgroud)

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  |
Run Code Online (Sandbox Code Playgroud)

删除book表行时:

DELETE FROM book 
WHERE id = 1
Run Code Online (Sandbox Code Playgroud)

book_audit_log通过表上的 AFTER DELETE 触发器添加一条新记录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  |
Run Code Online (Sandbox Code Playgroud)

就是这样!


dan*_*nda 5

我创建了一个名为cdc_audit的工具,它可以在 mysql 中为任何或所有表自动创建审计表,甚至保留预先存在的触发器。也许您或某人会发现它很有用

特征

  • 自动生成审计表
  • 自动生成触发器以填充审计表
  • 自动将审计表中的新行同步到 .csv 文件。
  • 读取 mysql information_schema 以自动确定表和列。
  • 可以为所有数据库表或指定列表生成表+触发器。
  • 可以同步所有数据库表或指定列表的审计表。
  • 在生成 AFTER 触发器时保留预先存在的触发器逻辑(如果有)。
  • 同步脚本选项删除除最后一个审计行之外的所有行,以保持源数据库较小。

更新:这是一个示例,在名为 stackoverflow 的测试数据库中使用上面的贷款表。

$ ./cdc_audit_gen_mysql.php -t loan -d stackoverflow

Successfully Generated Audit Tables + Triggers in ./cdc_audit_gen
Run Code Online (Sandbox Code Playgroud)

现在让我们运行 sql 在数据库中创建审计表和触发器。

$ mysql -u root stackoverflow < cdc_audit_gen/loan.audit.sql
Run Code Online (Sandbox Code Playgroud)

就是这样。审计表和触发器已就位。

如果好奇,我们可以检查实现。

$ 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;
Run Code Online (Sandbox Code Playgroud)


Hav*_*ame 1

试试这个,您的语法和分隔符中缺少分号

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 ;
Run Code Online (Sandbox Code Playgroud)