软删除字段必须为 DeletedAt gorm.DeletedAt 类型且不可重命名,否则触发硬删除;Find/First 默认跳过软删记录,查已删数据须用 Unscoped() 且必须置于 Where 等条件之前。

软删除字段必须用 gorm.DeletedAt 类型
GORM 的软删除机制只识别名为 DeletedAt 的 *time.Time 字段(即 gorm.DeletedAt 类型),其他字段名或类型(比如 IsDeleted、deleted_at 但类型是 bool 或 int)不会触发软删除逻辑。一旦字段命名或类型不符,调用 Delete() 会执行硬删除,且 Find() 不会自动过滤已删记录。
实操建议:
- 结构体中必须声明
DeletedAt gorm.DeletedAt `gorm:"index"`,加index提升查询性能 - 不要重命名该字段,哪怕加前缀(如
BaseDeletedAt)也会失效 - 若已有
deleted_at字段但类型是time.Time(非指针),需改为*time.Time并重命名为DeletedAt - 启用软删除后,所有
Find、First、Where默认跳过已软删记录——这是 GORM 的默认行为,无需额外配置
显式查出软删除数据要用 Unscoped()
日常查询几乎不会主动要已删数据,但归档、审计、恢复等场景必须绕过软删除过滤。GORM 提供 Unscoped() 方法取消全局软删除条件,但它不是“开关”,而是临时移除 WHERE 中的 deleted_at IS NULL 条件。
常见错误:在链式调用中误把 Unscoped() 放错位置,比如写成 db.Where("id = ?", id).Unscoped().First(&u) —— 这样 Where 条件仍生效,但软删条件被移除,可能查到不该查的已删记录;而 db.Unscoped().Where("id = ?", id).First(&u) 才是正确顺序。
实操建议:
-
Unscoped()必须放在Where、Joins等条件之前,否则部分条件可能不生效 - 归档时常用:
db.Unscoped().Where("deleted_at IS NOT NULL AND deleted_at - 恢复单条记录:先
Unscoped().First()查出,再Update("deleted_at", nil)清空时间戳 - 注意:调用
Unscoped()后所有后续操作(包括关联查询)都不再过滤软删数据,务必控制作用域
历史归档不能只靠 Unscoped() + Where
软删除只是标记,不代表数据已归档。直接在主表长期保留大量 DeletedAt != nil 记录,会导致主表膨胀、索引变慢、备份体积增大。真正的归档需要把历史数据迁出,而非仅加索引或分区。
实操建议:
- 归档动作应作为独立后台任务(如 cron job 或消息队列触发),避免阻塞 API 请求
- 迁移前用
db.Unscoped().Where(...).Select("id", "name", "deleted_at").Rows()流式读取,防止内存溢出 - 目标表结构需与原表一致,但去掉
DeletedAt字段(归档表不需软删除),并添加archived_at时间戳 - 迁移后执行
db.Unscoped().Where(...).Delete(&Model{})—— 注意这是硬删,确保已落库且不可逆 - 务必在事务外执行归档(GORM 事务不跨连接),并记录迁移起止 ID 和行数,便于断点续传
软删除与唯一约束冲突时很隐蔽
当表有唯一索引(如 email 字段),软删除后该值仍被索引锁定,新插入同值会报 duplicate key 错误。这是因为 PostgreSQL/MySQL 的唯一索引默认包含 NULL 值,而 DeletedAt 是 *time.Time,软删后为非 NULL 时间值,导致唯一性检查仍生效。
解决方法不是删索引,而是改用部分唯一索引(PostgreSQL)或函数索引(MySQL 8.0+)。例如 PostgreSQL 中:
CREATE UNIQUE INDEX idx_users_email_active ON users (email) WHERE deleted_at IS NULL;
这样只有未软删的记录参与唯一校验。MySQL 需用表达式索引(如 CREATE UNIQUE INDEX ... ON t ((IF(deleted_at IS NULL, email, NULL)))),但兼容性差,生产环境更推荐应用层校验 + 业务兜底。
容易踩的坑:
- 开发阶段用 SQLite 测试,它不支持部分索引,问题无法暴露
- DBA 未同步更新索引定义,上线后注册接口突然报唯一冲突
- 忽略
UPDATE场景:用户改邮箱时,需先检查新邮箱是否已被软删用户占用,不能只查活跃用户
软删除看着简单,但和索引、归档、事务、唯一性全搅在一起,每一步都得想清楚数据状态和边界条件。

















