gorm软删除需deletedat为*time.time类型,否则变物理删除;默认查询自动过滤软删记录,查全部需unscoped();恢复须unscoped().update("deleted_at", nil),物理删除必须显式unscoped()。

DeletedAt 字段类型写错,Delete() 就变物理删除
软删除不会自动生效,GORM 只在模型中检测到 DeletedAt 字段且类型为 gorm.DeletedAt(即 *time.Time)时才启用逻辑删除。常见错误包括:
-
DeletedAt time.Time:零值0001-01-01 00:00:00 +0000 UTC被判为“已删”,新记录一建就查不到 -
DeletedAt sql.NullTime或自定义时间包装类型:GORM 不识别,Delete()直接发DELETE FROM - 漏掉
gorm:"index"标签:功能不受影响,但高并发下WHERE deleted_at IS NULL查询可能无索引拖慢
最省事方案是直接嵌入 gorm.Model:
type User struct {
gorm.Model // 自带 ID, CreatedAt, UpdatedAt, DeletedAt *time.Time `gorm:"index"`
Name string
}
查不到软删记录?不是 bug,是默认过滤
GORM 所有常规查询(Find、First、Count、Where().Find)都会自动追加 WHERE deleted_at IS NULL。你“查不到”,恰恰说明软删除在工作。
- 查存活数据:直接
db.Where("name = ?", "foo").Find(&users)✅ - 查全部(含软删):必须
db.Unscoped().Where("name = ?", "foo").Find(&users)✅ - 只查已软删的:
db.Unscoped().Where("deleted_at IS NOT NULL").Find(&users)✅(注意字段名小写deleted_at)
Unscoped() 是链式调用,必须放在最前,且透传到后续所有操作(包括 Preload 关联查询),这点在带关联的管理接口里极易出错。
恢复软删记录不能用 Save(),得用 Unscoped().Update("deleted_at", nil)
恢复本质是把 DeletedAt 设回 nil,但以下写法都错:
-
user.DeletedAt = nil; db.Save(&user):可能跳过BeforeUpdate钩子,且若DeletedAt是time.Time类型,赋nil会 panic -
user.DeletedAt = time.Time{}:零值时间仍被视为已删
正确做法:
db.Unscoped().Model(&user).Where("id = ?", 123).Update("deleted_at", nil)批量恢复同理:db.Unscoped().Model(&User{}).Where("status = ?", "inactive").Update("deleted_at", nil)
物理删除必须显式加 Unscoped(),否则 Delete() 永远只是 UPDATE
db.Delete(&user) 默认只执行 UPDATE users SET deleted_at = ? WHERE ...;真要发 DELETE FROM,必须加 Unscoped():
- 单条物理删:
db.Unscoped().Delete(&user) - 批量物理删:
db.Unscoped().Where("created_at - 注意:主键为零值(如
ID = 0)时,db.Delete(&User{ID: 0})可能生成无条件语句,高风险场景务必先First()查出再删,或改用Where().Delete()
最容易被忽略的是:软删除是否生效,不只看字段名,而取决于类型(gorm.DeletedAt)、嵌入方式(gorm.Model 或显式声明)、以及标签(gorm:"index")三者缺一不可;而 Unscoped() 的透传性会让整个链路(含 Preload)都绕过过滤——这在带关联的回收站页面里,经常导致“看到用户但看不到其订单”的诡异现象。











