gorm分页查询时逻辑删除条件自动注入仅限find、first等标准链式方法;raw、session、pagedao等手动路径不触发,需显式添加deletedat is null过滤。

分页查询时逻辑删除条件是否自动注入
GORM 的逻辑删除不会自动注入到所有分页查询中,是否生效取决于你用的是哪种查询方式。只有调用 Find、First、Take 等标准链式方法时,才会触发全局软删除 Scope;而直接用 Raw、Session 或 Scopes 手动绕过时,DeletedAt IS NULL 条件就丢了。
常见错误现象包括:分页接口返回已“删除”的用户、后台管理列表出现异常数据、导出 Excel 包含历史归档记录。
- 使用
db.Offset(0).Limit(10).Find(&users)→ ✅ 自动过滤 - 使用
db.Raw("SELECT * FROM users LIMIT ?", 10).Scan(&users)→ ❌ 完全不走逻辑删除逻辑 - 使用
db.Session(&gorm.Session{NewDB: true}).Find(&users)→ ❌ 新建 Session 会丢失注册的全局 Scope
PageDao 类型分页器与 GORM 软删除的兼容性
像 goweb3 中的 PageDao 这类泛型分页框架,底层若直接拼接 SQL 或复用原生 database/sql 查询路径,大概率跳过 GORM 的软删除拦截器。它依赖的是元数据反射和手动构建 WHERE 条件,而不是 GORM 的 Scope 注册机制。
这意味着即使你在模型里定义了 DeletedAt gorm.DeletedAt,PageDao.QueryModel 返回的结果仍可能包含已删除数据,除非你显式在查询参数里加 is_deleted = false 或等价条件。
-
PageDao的QueryModel默认不感知 GORM 的软删除规则 - 若想兼容,需在构建
QueryModel前手动追加DeletedAt IS NULL过滤条件 - 缓存层(如
CacheFindId)同样不受 GORM Scope 影响,需单独处理软删除状态校验
ORDER BY + LIMIT 场景下 DeletedAt 索引失效风险
当分页查询带 ORDER BY created_at DESC 且未对 DeletedAt 建复合索引时,数据库优化器很可能放弃使用 created_at 索引,转而执行全表扫描——因为 GORM 插入的隐式条件 DeletedAt IS NULL 无法被单列索引高效覆盖。
典型慢查询日志会显示 type: ALL, rows: 248931,哪怕只查 10 条数据。
- 必须为
DeletedAt字段单独加INDEX(GORM 的gorm:"index"标签仅声明,不自动建索引) - 高并发分页场景建议建复合索引:
INDEX idx_deleted_created (DeletedAt, created_at) - PostgreSQL 用户注意:
IS NULL在 B-tree 索引中默认不高效,需配合WHERE DeletedAt IS NULL显式写法才能命中索引
自定义分页函数里如何安全保留软删除语义
如果你封装了类似 Paginate(db *gorm.DB, page, size int, out interface{}) 的工具函数,不能只依赖 db.Scopes(paginateScope),还必须确保传入的 *gorm.DB 实例已注册软删除 Scope,且未被 Session 或 Unscoped 破坏上下文。
最稳妥的做法是在函数内部显式重置 Scope,而非信任调用方传来的 db 实例。
- 避免直接接收外部
*gorm.DB,改用工厂函数生成带软删除的实例:db.WithContext(ctx).Session(&gorm.Session{}).Scopes(SoftDeleteScope) - 不要在分页前调用
db.Unscoped(),否则后续所有操作都失效 - 调试时可用
db.Session(&gorm.Session{DryRun: true}).Find(&u)查看实际生成的 SQL,确认是否含DeletedAt IS NULL
逻辑删除和分页组合使用时,最易被忽略的不是“怎么写”,而是“谁负责加条件”——GORM 只保证它自己走的链路,不保你封装的、手写的、缓存的、跨库的每一条路径。











