Revel 框架不提供软删除抽象层,需手动启用 GORM 的 DeletedAt 字段并显式校验;User.Find() 查不到已删记录因 GORM v2 自动追加 WHERE deleted_at IS NULL,而 raw SQL 不触发该逻辑;恢复软删记录须用 Unscoped().Where(...).First().UpdateColumn("deleted_at", nil) 并检查 RowsAffected。

Revel 框架本身不提供软删除抽象层,GORM 的 DeletedAt 字段必须由你显式启用、手动校验,并在所有查询中确保未被意外绕过——否则数据会“物理消失”或“查不到但其实存在”。
为什么 User.Find() 查不到已删记录,但 raw SQL 能查到
这是 GORM v2 默认启用软删除钩子导致的:只要模型含 gorm.DeletedAt 字段且类型为 *time.Time,所有 Find、First、Where 等查询自动追加 WHERE deleted_at IS NULL 条件。而原生 db.Raw() 或 db.Table().Select() 不走 GORM 查询构建器,自然不触发该逻辑。
- 检查你的模型是否真包含该字段:
DeletedAt gorm.DeletedAt `gorm:"index"`(注意 tag 名必须是gorm:"index",仅写gorm:"-"会禁用软删除) - 若想临时查出已软删记录,必须显式调用
Unscoped():db.Unscoped().First(&user, id) - 别在控制器里无条件用
Unscoped()——它会绕过所有软删除保护,暴露脏数据
如何安全恢复一条软删除记录
恢复不是“反向 delete”,而是把 DeletedAt 设为 nil。GORM 不提供 Restore() 方法,必须手写更新逻辑。
- 不能只写
db.Model(&user).Update("deleted_at", nil)—— 这会跳过 GORM 的钩子(如BeforeUpdate),且不校验记录是否存在 - 推荐做法:先查再改,确保原子性与存在性:
db.Unscoped().Where("id = ? AND deleted_at IS NOT NULL", id).First(&user).UpdateColumn("deleted_at", nil) -
UpdateColumn直接生成 SQL 更新,不触发回调和验证;Update则会跑完整生命周期,可能因其他字段为空而失败 - 操作后务必检查
result.RowsAffected,为 0 表示该记录根本没被软删过,或 ID 不存在
软删除 + Revel Controller 的常见坑
Revel 的 controller 层对 DeletedAt 完全无感,不会自动过滤、也不会自动设置。所有软删除语义都得你一手托底。
- CRUD 模板生成的
Destroy方法默认调用db.Delete()—— 这会软删,但前端点击“删除”后页面没反馈是否成功,因为没检查result.Error和result.RowsAffected - 列表页(
List)若直接用db.Find(&users),用户看不到已删数据,这通常符合预期;但若要做“回收站”功能,就必须用Unscoped().Where("deleted_at IS NOT NULL")显式查 - 别在 model 文件里写
func (u *User) Delete(db *gorm.DB) error { return db.Delete(u).Error }—— 这看似封装,实则掩盖了软删/硬删歧义;应拆成DeleteSoft和DeleteHard两个明确函数 - 事务中执行软删除时,记得
Unscoped()只作用于当前链式调用,不会污染后续语句
最易被忽略的是:GORM 的软删除只作用于主表字段,关联查询(如 Preload("Orders"))不会自动给 Orders 表加 deleted_at IS NULL 条件。如果订单表也有软删除字段,必须手动 Preload("Orders", "deleted_at IS NULL"),否则会拉出已被删的订单。


















