GORM软删除下Count返回0是因为默认添加WHERE deleted_at IS NULL条件;统计含已删数据需用Unscoped(),但分页时Count与Find必须同scope以避免错乱。

软删除下 Count 为什么总是返回 0?
因为 GORM 默认开启软删除,Count 方法在底层会自动加上 WHERE deleted_at IS NULL 条件。如果你查的是已被软删除的记录(即 deleted_at 非空),它们天然被排除在外 —— 所以 Count 结果为 0 并不是 bug,而是行为一致性的体现。
但问题常出现在:你想统计「所有逻辑存在」的数据总量(含已软删除的),或想单独统计「已软删除」的数量。这时候直接调用 Count 就会漏数据。
- 默认
db.Model(&User{}).Count(&total)等价于SELECT COUNT(*) FROM users WHERE deleted_at IS NULL - 若模型没嵌入
gorm.Model或未显式声明DeletedAt gorm.DeletedAt字段,GORM 不会启用软删除逻辑,Count也不加该条件 —— 此时行为反而“不一致” - 使用
Unscoped()是最直接的绕过方式,但它会完全忽略软删除标记,包括后续所有链式方法(如Where、Order)都运行在无作用域上下文中
Unscoped() 的正确用法和陷阱
Unscoped() 不是“临时关闭软删除”,而是“彻底移除所有默认 scope”,包括软删除、多租户字段(如 tenant_id)、状态过滤等。它只对紧随其后的操作生效,且不能“局部恢复”。
- ✅ 正确:统计含软删除的总条数 →
db.Unscoped().Model(&User{}).Count(&total) - ✅ 正确:只对 Count 取消软删除,但查询仍要过滤活跃用户 → 分两步:
db.Unscoped().Model(&User{}).Count(&total)+db.Where("status = ?", 1).Find(&users) - ❌ 错误:写成
db.Unscoped().Where(...).Count()再接Find()—— 后者不会继承Unscoped,容易造成总数和列表数据不匹配 - ⚠️ 注意:
Unscoped()不影响预加载(Preload)的关联表 scope,关联查询仍走各自模型的默认 scope
分页时 Count 和 Find 必须同 scope
gin-vue-admin 类项目常见错误:分页接口里先用 Unscoped().Count() 算总数,再用普通 db.Where(...).Limit().Offset().Find() 拿数据 —— 总数包含已删数据,列表却只显示未删的,前端翻页会错乱甚至 404。
- 分页总数必须与实际返回列表使用完全相同的查询条件和 scope
- 如果业务只要查“未删除 + 状态正常”的用户,那
Count就不该加Unscoped(),而应和Find共享同一组Where链式调用 - 示例(安全写法):
db := db.Where("status = ?", 1) db.Count(&total) // total 基于 status=1 计算 db.Offset(offset).Limit(limit).Find(&users) // users 也是 status=1 - 若需支持“查看已删除数据”的管理后台页,建议单独建一个 API 路由,明确用
Unscoped(),并在文档中标注 scope 行为
自定义软删除字段名时 Count 会失效吗?
会。GORM 只识别两种软删除字段声明方式:DeletedAt gorm.DeletedAt 或嵌入 gorm.Model。如果你自定义了字段名(比如叫 is_deleted int 类型),GORM 根本不会把它当软删除字段处理,Count 也不会自动加条件 —— 但此时 Delete() 也不会软删,而是物理删除。
- ✅ 推荐:坚持用
DeletedAt gorm.DeletedAt,这是 GORM 软删除契约的核心 - ❌ 避免:用
IsDeleted bool+ 自定义 scope,虽然能 work,但Count、Delete、First等所有方法都不会自动适配,需手动补全 every single query - ⚠️ 特别注意:
gorm.DeletedAt是结构体类型(含 time.Time 和 bool),不是单纯 time.Time;直接赋值nil或time.Time{}可能触发非预期行为,应使用gorm.DeletedAt{Time: time.Now(), Valid: true}或让 GORM 自动处理


















