【问题标题】:How to create audit trail or logging tables with triggers in MySQL如何在 MySQL 中使用触发器创建审计跟踪或日志记录表
【发布时间】:2015-03-16 07:28:48
【问题描述】:

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


“贷款”表:

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)
);

还有“过期”表

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 上收到“(我的)SQL 语法错误”,我不知道为什么。任何帮助将不胜感激!

【问题讨论】:

    标签: mysql sql tsql triggers audit-trail


    【解决方案1】:

    我创建了一个名为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
    

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

    $ mysql -u root stackoverflow < cdc_audit_gen/loan.audit.sql
    

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

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

    $ 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;
    

    【讨论】:

    • 您能否提供一个示例来改进您的帖子?谢谢!
    • 请不要向多个问题发布完全相同的答案(既不是直接副本也不是链接):它要么不适合所有人,要么问题是重复的,应该标记/关闭为这样的。另请注意help about promotion
    • 感谢@danda 的图书馆,我使用它并意识到您正在审计表中保存新值,并且根据我对审计跟踪表的理解,UPDATE 案例必须有旧值
    【解决方案2】:

    在 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)
    )
    
    • 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 语句

    现在,要输入审计日志表,您需要创建以下 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
    

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

    dml_type 列的值设置为INSERTUPDATEDELETEdml_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 表上执行 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表上执行的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 表行时:

    UPDATE book 
    SET price_in_cents = 4499 
    WHERE id = 1
    

    我们可以看到book 表上的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 表行时:

    DELETE FROM book 
    WHERE id = 1
    

    book 表上的 AFTER DELETE 触发器将新记录添加到 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  |
    | 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  |
    

    就是这样!

    【讨论】:

      【解决方案3】:

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

      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 ;
      

      【讨论】:

      • 不,还是同样的错误。我实际上已经有一个DROP IF EXISTS 声明,我只是忽略了将它包含在我的问题中。
      • 啊,小错误,加了分号就可以了。谢谢:)
      【解决方案4】:

      我知道我参加聚会为时已晚...不过,我使用的以下代码将有助于在任何数据库上的任何表名上创建审计试验。

      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
      

      【讨论】:

        猜你喜欢
        • 2016-09-15
        • 2011-02-16
        • 2010-10-09
        • 2021-06-22
        • 1970-01-01
        • 2011-01-09
        • 2017-01-09
        • 2021-05-21
        • 2017-03-15
        相关资源
        最近更新 更多