
本文详解为何直接通过表单隐藏字段操作 json 数据会导致评论被覆盖,并提供基于事务的原子性更新方案及更优的数据库范式设计(独立评论表),避免并发丢失与数据不一致问题。
在 PHP + MySQL 应用中,将结构化数据(如用户评论)以 JSON 格式存储到单个字段看似便捷,但若处理不当,极易引发数据覆盖而非追加的问题——正如你所遇到的:每次提交新评论后,旧评论全部消失,仅保留最新一条。
根本原因在于你的当前逻辑存在两个关键缺陷:
- 状态脱离数据库:你将 json_encode($item->issue_comments) 输出到 HTML 隐藏字段,使客户端持有 JSON 的“快照”。当表单提交时,PHP 仅基于该快照解码、添加新评论并重新编码。若在此期间其他用户已更新数据库,你的操作将覆盖他人新增的评论;
- 缺乏并发控制:没有使用数据库锁或事务机制,导致多个请求对同一记录的 JSON 字段执行“读–改–写”时发生竞态条件(race condition),后提交者必然覆盖先提交者的修改。
✅ 正确做法一:使用事务 + 行级锁(适用于必须用 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 更新难题,应将评论建模为独立实体:
立即学习“PHP免费学习笔记(深入)”;
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);总结
- ❌ 不要将 JSON 数据置于表单隐藏字段中来回传输——这是状态同步灾难的源头;
- ✅ 若坚持 JSON 存储,务必使用 SELECT ... FOR UPDATE + 事务保证读–改–写原子性;
- ✅ 最佳实践是遵循数据库范式,为评论建立独立表——它天然支持并发写入、索引优化、SQL 查询能力及未来扩展性(如点赞、编辑、删除审计等)。
结构清晰的数据模型,远胜于“方便”的反模式。











