恢复软删除记录必须用Unscoped()配合Where()和Update("DeletedAt", nil),漏掉Unscoped()则WHERE自动添加deleted_at IS NULL导致查不到;Unscoped()须置于Where()之前,且不能用Save()或原生SQL替代。

软删除恢复必须用 Unscoped() 配合 Update()
调 db.Model(&u).Update("DeletedAt", nil) 一定失败——GORM 会在 WHERE 后自动加 deleted_at IS NULL,而你要恢复的记录恰恰是 deleted_at IS NOT NULL,根本查不到。真正能生效的写法只有一种:db.Unscoped().Where("id = ?", u.ID).Update("DeletedAt", nil)。
常见错误现象:执行后 RowsAffected == 0,日志里看到 SQL 是 UPDATE ... WHERE id = 123 AND deleted_at IS NULL,说明没加 Unscoped() 或位置写错了(比如写成 db.Where(...).Unscoped().Update(...),链式顺序错)。
-
Unscoped()必须放在Model()或Where()之前,否则不生效 - 别用
Save()恢复:它会把整个 struct 写回去,可能覆盖UpdatedAt等字段,且仍受软删除过滤 - 如果主键不是
ID(比如复合主键或自定义主键名),Where()条件必须完整匹配,否则更新零行
Unscoped() 不是“查所有”,而是“跳过软删除过滤”
很多人以为 db.Unscoped().Find(&users) 就能拿到全部数据,结果发现关联表(如 Preload("Orders"))里的已删订单还是没加载出来——因为 Unscoped() 只作用于主查询,不自动透传到预加载的关联表。
要连带查出已删的关联记录,得显式给关联模型也加 Unscoped():db.Unscoped().Preload("Orders", func(db *gorm.DB) *gorm.DB { return db.Unscoped() }).Find(&users)。
立即学习“go语言免费学习笔记(深入)”;
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 默认
Preload会自动过滤掉软删除的关联记录,这是 GORM 的设计,不是 bug -
Unscoped()是链式方法,一旦调用,后续所有操作(包括事务内)都跳过软删除逻辑,慎用于业务主流程 - 后台管理、回收站、审计等场景才适合用
Unscoped(),普通用户列表页不该暴露已删数据
恢复失败的三个典型原因
恢复操作看似简单,但实际踩坑最多。最常遇到的是:
- 字段类型不对:用了
time.Time而非*time.Time或gorm.DeletedAt,导致nil无法赋值,Update("DeletedAt", nil)实际发的是空字符串或零时间 - 索引缺失:没加
gorm:"index",高并发下WHERE deleted_at IS NOT NULL查询变慢,恢复操作卡住或超时 - 手动迁移遗漏:已有表加了
deleted_at字段但没跑AutoMigrate(),GORM 不识别该字段为软删除标记,Delete()仍走物理删除
别用原生 SQL 或 Raw() 绕过软删除
写 db.Raw("SELECT * FROM users").Scan(&users) 确实能查到已删数据,但这等于完全绕过 GORM 的软删除机制——你得自己补 AND deleted_at IS NULL 才安全,漏一处就导致业务逻辑错乱(比如用户看到自己已删的订单)。
更危险的是,用 Raw() 做恢复:db.Exec("UPDATE users SET deleted_at = NULL WHERE id = ?", id),这会跳过 GORM 的钩子(如 BeforeUpdate),也绕过事务控制,和 ORM 层状态脱节。
- 坚持用 GORM 提供的
Unscoped()+Update()组合,语义清晰、事务安全、可测试 - 如果真要用原生 SQL,务必在所有查询中手动补全软删除条件,且只限于导出、归档等离线场景
- 恢复操作建议包在事务里:
db.Transaction(func(tx *gorm.DB) error { return tx.Unscoped().Where(...).Update(...).Error }),避免部分成功
恢复动作本身不复杂,但容易忽略字段类型、链式调用顺序、关联表透传这三个点。尤其是 Unscoped() 的作用域边界,一不小心就会让本该隔离的逻辑互相污染。

















