软删除字段必须是gorm.deletedat类型且字段名只能为deletedat,需加gorm:"index";恢复操作须用unscoped().model().where().update("deleted_at", nil),传nil而非零值时间,且要手动处理关联数据。

软删除字段必须是 gorm.DeletedAt 类型
GORM 的软删除机制依赖于一个名为 DeletedAt 的字段,且类型必须是 *time.Time(即 gorm.DeletedAt 别名)。如果结构体里用的是 time.Time、int64 或自定义时间字段(比如 DelTime),Unscoped() 无法识别为软删除标记,恢复操作会静默失败。
正确写法:
type User struct {
ID uint `gorm:"primaryKey"`
Name string
DeletedAt gorm.DeletedAt `gorm:"index"` // 必须这样声明
}
- 字段名必须叫
DeletedAt,不能改;GORM 不支持通过 tag 自定义软删除字段名 - 必须加
gorm:"index",否则Unscoped().Where("deleted_at IS NOT NULL")查询效率低 - 如果已有表且字段是
del_time BIGINT,需先迁移字段并补全历史数据为NULL,再改结构体
恢复操作必须用 Unscoped().Model().Where().Update()
不能用 Save() 或普通 Update(),因为默认作用域会自动过滤掉已软删除的记录。必须显式调用 Unscoped() 绕过软删除条件,并用 Model(&u).Where(...) 定位目标行,再清空 DeletedAt。
示例(恢复 ID 为 123 的用户):
db.Unscoped().Model(&User{}).Where("id = ?", 123).Update("deleted_at", nil)
-
Update("deleted_at", nil)是关键:传nil才会把数据库字段设为NULL;传time.Time{}会写入零值时间,仍算“已删除” - 不能写成
db.Unscoped().First(&u, 123); u.DeletedAt = nil; db.Save(&u)—— 这样Save会忽略DeletedAt字段(GORM 默认不更新零值时间) - 批量恢复时,
Where("deleted_at IS NOT NULL")比Where("id IN ?")更安全,避免误恢复本就未删的记录
查询时 Unscoped() 和 Unscoped(true) 行为不同
Unscoped() 默认只跳过软删除过滤,但保留其它全局 scope(比如租户隔离 scope);而 Unscoped(true) 会彻底禁用所有 scope,包括你自定义的 func(db *gorm.DB) *gorm.DB。恢复数据前务必确认是否需要绕过全部 scope。
- 仅恢复软删除:用
db.Unscoped().Model(...).Where(...).Update(...) - 要同时绕过租户限制(如超级管理员恢复其他租户数据):用
db.Unscoped(true).Model(...) - 错误用法:
db.Unscoped().Where("id = ?", x).Delete(&User{})—— 这会触发硬删除,不是恢复
恢复后关联数据不会自动刷新
GORM 不会在执行 Update("deleted_at", nil) 后自动重新加载该记录的预加载(Preload)或关联字段。如果业务逻辑依赖关联数据(比如恢复订单时需同步恢复其 OrderItems),必须手动处理。
- 软删除恢复本身只影响当前模型的
DeletedAt字段,不会触发表关联的软删除状态 - 若关联表也有软删除(如
OrderItem也含DeletedAt),需单独执行恢复语句,不能指望级联 - 恢复后立即查数据,建议加
db.Unscoped().Joins("JOIN ...").Where(...)显式拉取最新状态,别依赖之前缓存的 struct 值
nil 传递、scope 范围和关联一致性这四点,任何一个出错都会导致“以为恢复了,其实没生效”。尤其在多环境部署时,容易因结构体定义不一致或迁移遗漏踩坑。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











