事务中Delete()仍是软删除,生成UPDATE语句而非DELETE;需Unscoped().Delete()强制物理删除;Before/AfterDelete回调在事务中生效,可校验或级联软删子表。

事务中调用 Delete() 仍是软删除,不是物理删除
在 db.Transaction() 里执行 Delete(),只要模型含合法 DeletedAt 字段,GORM 仍会生成 UPDATE ... SET deleted_at = ?,不会变成 DELETE FROM。事务只保证原子性,不改变软删除语义。
常见错误现象:以为“进了事务就能回滚硬删”,结果发现数据只是被标记删除,且事务回滚后 DeletedAt 也被撤销——这其实是正确行为,因为软删除本身就是一次 UPDATE 操作。
- 事务内
db.Delete(&user)→ SQL 是UPDATE users SET deleted_at = '...' WHERE id = ? - 事务中途
tx.Rollback()→DeletedAt回滚为nil,记录恢复可见 - 若想在事务中强制物理删除(极少见),必须显式写
tx.Unscoped().Delete(&user)
Unscoped() 在事务里必须提前调用
Unscoped() 是链式方法,它影响后续所有查询/操作的 WHERE 条件。在事务中若需查或删已软删数据,Unscoped() 必须出现在 Where、First、Delete 之前,否则条件仍受软删除过滤。
错误写法:tx.Where("id = ?", 123).Unscoped().First(&u) —— 这里 Where 先执行,已按默认规则加了 deleted_at IS NULL,Unscoped() 晚了没用。
- 正确顺序:
tx.Unscoped().Where("id = ?", 123).First(&u) - 批量恢复场景:
tx.Unscoped().Where("deleted_at IS NOT NULL AND status = ?", "draft").Update("deleted_at", nil) - 注意:
Unscoped()的作用域仅限当前链式调用,不影响其他独立 tx 方法
BeforeDelete / AfterDelete 回调在事务中生效
软删除触发的 BeforeDelete 和 AfterDelete 回调,和普通删除一样,在事务上下文中执行。这意味着你可以在回调里做校验、写日志、甚至操作其他表,这些操作都会随事务一起提交或回滚。
典型用途:禁止删除关联未清理的订单,或同步写入回收站快照。
-
BeforeDelete里可 return error 中断事务:if u.OrderCount > 0 { return errors.New("order exists") } -
AfterDelete里读取完整*u是安全的(字段未被清空),适合序列化存档:tx.Create(&RecycledItem{...}) - 别在
BeforeDelete里调tx.Delete()同一模型,易引发递归或死锁
软删除 + 外键级联需手动处理
GORM 不自动处理软删除下的外键约束。比如 User 软删后,其 Orders 默认不会被软删或标记;数据库外键(如 ON DELETE CASCADE)也只响应物理 DELETE,对 UPDATE deleted_at 无反应。
这意味着:事务中软删父记录,子记录状态不变,可能造成数据不一致。
- 方案一:在
AfterDelete回调里手动软删子表:tx.Model(&Order{}).Where("user_id = ?", u.ID).Update("deleted_at", time.Now()) - 方案二:用数据库原生外键 + 触发器(不推荐,脱离 ORM 管控)
- 方案三:业务层约定“查用户订单时 always Unscoped().Where("user_id = ? AND deleted_at IS NULL", u.ID)”——但得确保所有查询都记得加条件
最易被忽略的是:级联软删除必须在同一个事务中完成,否则回调里新建的 tx 或直接 db 调用会脱离当前事务边界。


















