本文详解为何直接通过表单隐藏字段操作 json 数据会导致评论被覆盖,并提供基于事务的原子性更新方案及更优的数据库范式设计(独立评论表),避免并发丢失与数据不一致问题。
在 PHP + MySQL 应用中,将结构化数据(如用户评论)以 JSON 格式存储到单个字段看似便捷,但若处理不当,极易引发数据覆盖而非追加的问题——正如你所遇到的:每次提交新评论后,旧评论全部消失,仅保留最新一条。
根本原因在于你的当前逻辑存在两个关键缺陷:
确保操作具备原子性,需在数据库层面锁定目标行,再完成读取、修改、写入全流程:
try {
$pdo->beginTransaction();
// 1. 查询并加锁(FOR UPDATE 防止并发修改)
$stmt = $pdo->prepare("SELECT issue_comments FROM issues WHERE id = ? FOR UPDATE");
$stmt->execute([$issue_id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$comments = $row['issue_comments'] ? json_decode($row['issue_comments'], true) : ['comment' => []];
// 2. 安全追加新评论(注意:确保 'comment' 键始终为数组)
if (!isset($comments['comment']) || !is_array($comments['comment'])) {
$comments['comment'] = [];
}
$comments['comment'][] = [
'date' => date('Y-m-d H:i:s'),
'name' => $issue_commenter,
'comment' => $_POST['issue_comment'] ?? ''
];
// 3. 写回数据库
$updateStmt = $pdo->prepare("UPDATE issues SET issue_comments = ? WHERE id = ?");
$updateStmt->execute([json_encode($comments, JSON_UNESCAPED_UNICODE), $issue_id]);
$pdo->commit();
} catch (Exception $e) {
$pdo->rollback();
throw $e;
}⚠️ 注意事项:必须使用支持事务的存储引擎(如 InnoDB);FOR UPDATE 仅在事务内生效,且会阻塞其他对该行的 SELECT ... FOR UPDATE 或 UPDATE 操作,需合理评估性能影响;始终校验 JSON 解码结果,避免 null 导致后续 [] 操作失败。
从根本上规避 JSON 更新难题,应将评论建模为独立实体:
CREATE TABLE issue_comments (
id INT PRIMARY KEY AUTO_INCREMENT,
issue_id INT NOT NULL,
commenter_name VARCHAR(100),
comment_text TEXT,
created_at DATETIME DEFAULT
CURRENT_TIMESTAMP,
INDEX idx_issue_id (issue_id)
);添加评论只需简单插入:
$stmt = $pdo->prepare(
"INSERT INTO issue_comments (issue_id, commenter_name, comment_text) VALUES (?, ?, ?)"
);
$stmt->execute([$issue_id, $issue_commenter, $_POST['issue_comment']]);查询所有评论也更高效、可索引、支持分页与聚合:
$stmt = $pdo->prepare(
"SELECT commenter_name, comment_text, created_at
FROM issue_comments
WHERE issue_id = ?
ORDER BY created_at DESC"
);
$stmt->execute([$issue_id]);
$comments = $stmt->fetchAll(PDO::FETCH_ASSOC);结构清晰的数据模型,远胜于“方便”的反模式。