GORM软删除字段名必须为DeletedAt且类型为*time.Time,否则触发硬删除;默认查询自动过滤已软删数据,需Unscoped()查全部;Delete方法执行UPDATE而非DELETE。

软删除字段名必须是 DeletedAt
GORM 的软删除机制是硬编码识别的,只有字段名为 DeletedAt 且类型为 *time.Time(或实现了 Scanner/Valuer 的时间类型)时,才会被自动识别为软删除字段。其他名字比如 is_deleted、deleted_at(小写)、SoftDeleteTime 都不会触发 GORM 的软删除逻辑。
常见错误现象:db.Delete(&user) 执行后数据从数据库消失(真删),或者查询时始终查不到已“删除”的记录——大概率是字段命名/类型不匹配。
- 结构体中必须声明为
DeletedAt *time.Time `gorm:"index"`(加index提升查询性能) - 不能用
uint或bool模拟软删除,GORM 不会识别 - 如果已有表结构含自定义删除字段,需先重命名列并迁移数据,否则 GORM 无法接管
Delete 方法调用即软删除,无需手动赋值
调用 db.Delete(&user) 或 db.Delete(&User{}, id) 时,GORM 会自动将当前时间写入 DeletedAt 字段,并跳过物理删除。前提是该模型已嵌入 gorm.Model 或自行定义了 DeletedAt 字段。
使用场景:用户注销、文章下架、订单取消等需要保留历史但隐藏展示的业务。
- 执行后 SQL 是
UPDATE ... SET deleted_at = '2024-05-20 10:30:00' WHERE ...,不是DELETE FROM - 若想强制物理删除,得显式用
Unscoped().Delete() - 批量删除(如
db.Where("status = ?", "draft").Delete(&Post{}))同样走软删逻辑
查询默认自动过滤已软删除的数据
GORM 默认在所有 Find、First、Where 等查询方法中自动添加 WHERE deleted_at IS NULL 条件。这是通过内置的 scope 实现的,不需要额外配置。
容易踩的坑:想查“包括已删除的全部记录”时忘记用 Unscoped(),导致数据对不上。
-
db.First(&user, 123)查不到DeletedAt != nil的记录 -
db.Unscoped().First(&user, 123)可查到(无论是否软删除) -
db.Unscoped().Where("deleted_at IS NOT NULL").Find(&users)用于专门查已删除数据 - 注意:关联查询(如
Preload)也受此影响,需在预加载时单独加Unscoped
自定义软删除行为需覆盖 BeforeDelete 回调
如果需要在软删除前做校验(如禁止删除已支付订单)、或同步更新其他字段(如设置 Status 为 "deleted"),不能依赖外部逻辑,必须用 GORM 回调。
性能影响:回调函数每次 Delete 都执行,避免在其中做耗时操作(如 HTTP 调用、复杂计算)。
- 在模型中实现
BeforeDelete(tx *gorm.DB) error方法 - 回调内可通过
tx.Statement.ReflectValue获取当前实例 - 若返回非 nil 错误,整个软删除事务会中止(
tx.Error被设为该错误) - 慎用
tx.Unscoped().Delete()在回调里——这会导致循环调用,栈溢出
软删除真正难的不是启用,而是全链路一致性:迁移脚本要补全 DeletedAt 列,API 层要明确区分“查全部”和“查有效”,后台任务清理逻辑得适配 Unscoped。漏掉任意一环,数据语义就乱了。


















