在PHP开发的项目中,数据库数据的增删改操作如果没有留存记录,后续出现数据异常时很难定位问题,数据追溯查询和操作历史记录追踪就成为保障数据安全、实现操作审计的重要需求。本文会介绍完整的实现方案,帮助开发者落地相关功能。

一、核心实现思路
数据追溯的核心是为每一次数据库操作留存可追溯的记录,通常需要两步配合:第一步设计专门的操作日志表,存储操作相关的关键信息;第二步在业务的数据库操作入口处插入日志记录逻辑,将操作和实际数据变更关联起来。
1. 操作日志表设计
首先需要创建一张操作日志表,用来存储所有数据操作的痕迹,表结构可以参考以下设计:
CREATE TABLE `db_operation_log` ( `id` int(11) NOT NULL AUTO_INCREMENT, `table_name` varchar(50) NOT NULL COMMENT '操作的数据库表名', `operation_type` varchar(20) NOT NULL COMMENT '操作类型:insert/update/delete', `primary_key_val` varchar(100) DEFAULT NULL COMMENT '操作数据的主键值', `old_data` text COMMENT '操作前的数据,JSON格式', `new_data` text COMMENT '操作后的数据,JSON格式', `operator_id` int(11) NOT NULL COMMENT '操作人ID', `operator_ip` varchar(50) DEFAULT NULL COMMENT '操作人IP', `create_time` datetime NOT NULL COMMENT '操作时间', PRIMARY KEY (`id`), KEY `idx_table_key` (`table_name`,`primary_key_val`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库操作日志表';
2. 日志记录触发逻辑
可以在封装好的数据库操作类中添加日志写入方法,在每次执行增删改操作前,先查询当前数据状态,操作完成后将新旧数据、操作信息等写入日志表。
二、PHP实现示例
以下是一个简单的PDO操作类扩展示例,包含操作日志记录的完整逻辑:
<?php
class DbWithLog {
private $pdo;
private $operatorId; // 当前操作人ID
private $operatorIp; // 当前操作人IP
public function __construct($dsn, $username, $password, $operatorId, $operatorIp) {
$this->pdo = new PDO($dsn, $username, $password);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->operatorId = $operatorId;
$this->operatorIp = $operatorIp;
}
/**
* 执行更新操作并记录日志
* @param string $table 表名
* @param array $data 更新的数据 key=>value
* @param string $where where条件
* @param array $whereParams where条件参数
* @return int 影响行数
*/
public function updateWithLog($table, $data, $where, $whereParams = []) {
// 先查询更新前的数据,假设表主键为id
$primaryKey = 'id';
$oldSql = "SELECT * FROM `$table` WHERE $where LIMIT 1";
$oldStmt = $this->pdo->prepare($oldSql);
$oldStmt->execute($whereParams);
$oldData = $oldStmt->fetch(PDO::FETCH_ASSOC);
if (!$oldData) {
throw new Exception('待更新的数据不存在');
}
$primaryKeyVal = $oldData[$primaryKey];
// 执行更新操作
$setStr = implode(', ', array_map(function($key) {
return "`$key` = ?";
}, array_keys($data)));
$updateSql = "UPDATE `$table` SET $setStr WHERE $where";
$updateParams = array_merge(array_values($data), $whereParams);
$updateStmt = $this->pdo->prepare($updateSql);
$updateStmt->execute($updateParams);
$affectRows = $updateStmt->rowCount();
// 查询更新后的数据
$newSql = "SELECT * FROM `$table` WHERE `$primaryKey` = ? LIMIT 1";
$newStmt = $this->pdo->prepare($newSql);
$newStmt->execute([$primaryKeyVal]);
$newData = $newStmt->fetch(PDO::FETCH_ASSOC);
// 写入操作日志
$this->writeLog($table, 'update', $primaryKeyVal, $oldData, $newData);
return $affectRows;
}
/**
* 写入操作日志
*/
private function writeLog($table, $operationType, $primaryKeyVal, $oldData, $newData) {
$sql = "INSERT INTO `db_operation_log` (`table_name`, `operation_type`, `primary_key_val`, `old_data`, `new_data`, `operator_id`, `operator_ip`, `create_time`)
VALUES (?, ?, ?, ?, ?, ?, ?, NOW())";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
$table,
$operationType,
$primaryKeyVal,
$oldData ? json_encode($oldData, JSON_UNESCAPED_UNICODE) : null,
$newData ? json_encode($newData, JSON_UNESCAPED_UNICODE) : null,
$this->operatorId,
$this->operatorIp
]);
}
}
// 使用示例
$dsn = 'mysql:host=127.0.0.1;dbname=test;charset=utf8mb4';
$db = new DbWithLog($dsn, 'root', '123456', 1001, '192.168.0.100');
// 更新用户表中id为5的用户名
$db->updateWithLog('user', ['username' => 'new_name'], 'id = ?', [5]);
?>三、追溯查询实现
有了操作日志表之后,数据追溯查询就只需要根据条件查询日志表即可,比如查询某条用户数据的所有修改记录:
-- 查询user表主键为5的所有操作记录,按时间倒序 SELECT * FROM `db_operation_log` WHERE `table_name` = 'user' AND `primary_key_val` = '5' ORDER BY `create_time` DESC;
四、注意事项
- 日志表的数据量会随操作次数增长,建议定期归档历史数据,避免影响查询性能
- 对于敏感操作,可以额外记录操作人的终端信息、请求参数等内容,强化审计能力
- 如果业务使用的是ORM框架,可以在ORM的更新、删除事件钩子中插入日志记录逻辑,无需修改现有业务代码
- 日志表的数据和原业务数据需要存储在不同的数据库中,避免原数据库故障导致日志丢失