软删除应使用deleted_at字段而非is_deleted布尔值,GORM原生支持该字段并自动过滤;需分离软删与硬删路由,关联数据需手动或事件驱动处理,恢复前须校验外键、唯一约束等隐性条件。

软删除字段怎么设计才不影响现有查询逻辑
软删除本质是用状态标记代替物理删除,Gin 本身不处理这个逻辑,关键在数据库字段和 ORM 层。直接加 deleted_at 字段最稳妥,尤其配合 GORM —— 它原生识别该字段名,自动在查询中过滤掉已“删除”的记录。别用 is_deleted 布尔字段,否则每次查询都得手动加 WHERE is_deleted = false,漏一次就数据泄露。
实际操作建议:
- 数据库表中添加
deleted_at字段,类型为DATETIME或TIMESTAMP(MySQL)或timestamp with time zone(PostgreSQL) - GORM 模型结构体里声明
DeletedAt gorm.DeletedAt `gorm:"index"`,gorm.DeletedAt是封装好的可空时间类型 - 避免在模型里额外加
IsDeleted bool字段——和DeletedAt语义重复,且 GORM 不会自动同步两者 - 如果已有业务强依赖
id主键做关联(比如日志、审计表),软删除后仍需保留该id可查,不能清空或改写
Gin 路由里怎么区分软删除和硬删除请求
别把两种操作塞进同一个 DELETE /users/:id 接口。用户点击“删除”时默认软删,管理员后台点“彻底删除”才走硬删——这是最清晰的权限与语义分离方式。
推荐做法:
立即学习“go语言免费学习笔记(深入)”;
- 软删除用
DELETE /api/v1/users/:id,内部调用db.Unscoped().Where("id = ?", id).Delete(&user)是错的;正确做法是db.Delete(&user, id)(GORM 默认启用软删除) - 彻底删除单独开路由,比如
DELETE /api/v1/users/:id/permanent, handler 中必须显式调用db.Unscoped().Delete(&user, id) - 在 permanent 接口里加角色校验,例如
if !claims.IsAdmin { c.AbortWithStatusJSON(403, gin.H{"error": "forbidden"}) } - 别在 URL 参数里传
?force=true控制行为——容易被绕过,也违反 REST 对资源操作的语义约定
软删除后关联数据怎么同步处理
GORM 的 CASCADE 不会自动触发关联表的软删除,这是高频踩坑点。比如删除用户时,其发布的文章、评论默认不会被标记删除,变成悬空数据。
解决方案分三层:
- 数据库外键不设
ON DELETE CASCADE(它只触发物理删除,对软删无效) - 在主模型的
BeforeDelete回调里手动处理:例如func (u *User) BeforeDelete(tx *gorm.DB) error { return tx.Model(&Post{}).Where("author_id = ?", u.ID).Update("deleted_at", time.Now()).Error } - 若关联关系复杂(如多级嵌套),改用事件驱动:软删用户后发消息到本地 channel 或 Redis pub/sub,由监听协程异步更新关联表,避免阻塞 HTTP 请求
- 注意时间戳一致性:所有软删除操作尽量用同一时刻(如
time.Now().UTC()),否则按deleted_at查询时可能出现窗口偏差
恢复软删除数据时要注意哪些隐性约束
恢复不是简单把 deleted_at 设为 nil 就完事。真实场景中常遇到外键冲突、唯一索引重叠、状态机越权等问题。
- 恢复前先检查关联资源是否还存在且未被软删除,例如用户恢复前确认其所属部门
dept.deleted_at == nil - 如果有唯一约束字段(如
email),需确保软删除期间没被新用户占用,否则UPDATE SET deleted_at = NULL会因唯一键冲突失败 - GORM 的
Unscoped().First()能查出软删记录,但恢复操作要用Unscoped().Model(&u).Updates(map[string]interface{}{"deleted_at": nil}),不能用Save()——后者会忽略零值字段 - 前端恢复按钮应禁用 3 秒并显示“正在验证”,因为恢复可能涉及跨表校验,耗时不可忽略
软删除不是加个字段就万事大吉,真正麻烦的是状态扩散——一个软删动作要牵动权限、审计、搜索、缓存、关联链路。动手前先画清楚这张状态流转图,比写十行代码重要。


















