GORM软删除需同时满足DeletedAt字段为gorm.DeletedAt类型且主键有效,否则执行物理删除;默认查询自动过滤已删记录,恢复须用Unscoped().Update("deleted_at", nil)。

软删除不是加个字段就自动生效,GORM 只在结构体中识别 gorm.DeletedAt 类型的 DeletedAt 字段,并配合索引和主键有效性,才启用 UPDATE 而非 DELETE 行为。
为什么 Delete() 有时是物理删除,有时只是更新?
GORM 判断是否走软删除,只看两点:结构体里有没有类型为 gorm.DeletedAt 的 DeletedAt 字段,以及调用 Delete() 时传入对象的主键是否有效。
- 字段类型不对(比如写成
time.Time或sql.NullTime)→ GORM 当普通字段处理,Delete()发出DELETE FROM - 字段类型对但漏了
gorm:"index"→ 查询时可能漏掉该行(尤其在复合条件或关联查询中) - 主键为零值(如
ID = 0)→ GORM 生成无主键条件语句,高风险下可能误删全表 - 正确嵌入
gorm.Model是最省事方案,它已内置*time.Time类型的DeletedAt和索引
查不到软删除记录?默认行为就是过滤掉
GORM 所有常规查询方法(First()、Find()、Count()、Where().Find())都会自动追加 WHERE deleted_at IS NULL。这不是 bug,是设计前提。
- 想查含已删记录:必须前置
Unscoped(),例如db.Unscoped().Where("name = ?", "foo").Find(&users) - 只查已删记录:用
db.Unscoped().Where("deleted_at IS NOT NULL").Find(&users)(注意字段名小写) -
Unscoped()是链式调用,必须写在最前,且影响整条链(包括Preload()) - 统计总注册数这类指标,别忘了用
Unscoped().Count(),否则漏掉已删数据
恢复软删除记录,不能靠 Save() 赋 nil
恢复的本质是把 DeletedAt 设回 nil,但手动赋值再 Save() 会跳过钩子,还容易误写成 time.Time{}(零值时间仍被视为已删)。
- ✅ 正确做法:
db.Unscoped().Model(&user).Where("id = ?", 123).Update("deleted_at", nil) - ❌ 错误操作:
user.DeletedAt = nil; db.Save(&user)—— 不触发BeforeUpdate钩子,且字段未被 GORM 标记为“待更新” - 事务中恢复 + 后续更新要放在同一事务内,否则中间状态可能被其他协程读到脏数据
级联与归档:软删除不等于自动清理关联数据
GORM 软删除本身不处理外键关联,BeforeDelete 钩子也无法可靠加载未预加载的关联记录。应用层遍历删除易出错、低效、不原子。
- ✅ 推荐方案:在数据库层面建外键并设
ON DELETE CASCADE,由数据库引擎保障一致性 - 例如为
Employee表的company_id字段添加外键约束:db.Model(&Employee{}).AddForeignKey("company_id", "companies(id)", "CASCADE", "NO ACTION") - 定期归档需手动实现:用
Unscoped()查出旧软删记录,导出后执行Unscoped().Delete() - 别依赖
DeletedAt字段做业务逻辑判断(如“7天后自动清除”),应另起定时任务扫描 + 归档
软删除机制的复杂点不在字段定义,而在它如何与主键、索引、链式作用域(Unscoped())、事务边界、外键约束交织在一起——漏掉任一环,都可能让“软删”变成静默失效或意外硬删。


















