多租户分页必须显式、重复加 tenant_id 条件于 Count 和 Find,禁用全局注入;总数查询需新 DB 实例;大数据量租户须用带租户约束的游标分页;Preload 会破坏隔离,应改用两步查+IN。

多租户场景下直接套用常规 Limit + Offset 分页会漏数据或重复,核心矛盾在于:租户隔离条件(如 tenant_id = ?)必须严格参与总数统计和分页查询,且不能被链式调用意外污染。
租户字段必须显式写进 Where,别靠中间件“全局注入”
常见错误是用 db.Session(&gorm.Session{NewDB: true}) 或自定义 Scope 在中间件里统一加 Where("tenant_id = ?", tenantID),但后续 Count() 和 Find() 若复用同一 DB 实例,Where 条件可能残留或丢失。
- 每次分页必须手动、显式调用
Where("tenant_id = ?", tenantID),两次都写,不省略 - 避免用
Preload加载跨租户关联表(如Preload("Company")),除非 Company 表也带tenant_id字段并做了同样过滤 - 如果租户 ID 来自 JWT 或 Header,解析后立刻校验非空、合法,再传入
Where,别等到Count()时才检查
总数查询必须隔离租户上下文,不能复用分页链
db.Where("tenant_id = ?", tID).Count(&total) 看似正确,但如果这行代码写在 Offset().Limit() 后面,GORM 会把 Offset 和 Limit 也带上——结果永远 ≤ PageSize。
- 总数查询必须另起一个干净的
*gorm.DB实例:db.Session(&gorm.Session{NewDB: true}).Model(&User{}).Where("tenant_id = ?", tID).Count(&total) - 若涉及复杂 JOIN(如查用户+角色+权限),
Count极易因去重逻辑出错,改用手写子查询:db.Raw("SELECT COUNT(*) FROM (SELECT DISTINCT u.id FROM users u JOIN roles r ON u.role_id = r.id WHERE u.tenant_id = ?) t", tID).Scan(&total) - 租户数据量差异大时(如 SaaS 中有的租户有百万用户,有的只有 5 条),别缓存
Count结果,除非业务明确允许误差
高偏移量租户必须切游标分页,不能硬扛 Offset
租户 A 有 50 万用户,Offset(499999) 在 MySQL 上实际扫描 50 万行再丢弃,响应从 20ms 涨到 1.8s——这不是 GORM 的问题,是 SQL 执行模型决定的。
- 游标字段必须含租户约束:
WHERE tenant_id = ? AND id > ? ORDER BY id LIMIT 20,不能只写id > ? - 前端首次请求不传游标,后端查
ORDER BY id ASC LIMIT 20;后续请求传last_id,且该值必须来自同租户数据 - 时间戳游标(如
created_at)要加id做二级排序:WHERE tenant_id = ? AND (created_at, id) > (?, ?) ORDER BY created_at ASC, id ASC LIMIT 20,防止时间重复导致漏数据
Preload 关联分页在多租户下天然失效
写 db.Where("tenant_id = ?", tID).Preload("Orders").Offset().Limit().Find(&users),GORM 会先取 10 个用户,再发一条 SELECT * FROM orders WHERE user_id IN (1,2,...,10)——但这条语句没带 tenant_id,可能查出其他租户的订单。
- 禁止在分页主查询中用
Preload加载任何关联表 - 改为两步:先查用户 ID 列表
db.Where("tenant_id = ?", tID).Select("id").Offset().Limit().Find(&userIDs);再用IN批量查订单:db.Where("tenant_id = ? AND user_id IN ?", tID, userIDs).Find(&orders) - 如果关联表本身也需分页(如每个用户只取最新 3 个订单),用窗口函数或子查询,别指望 GORM 自动处理
多租户分页最易被忽略的是:租户隔离不是一次写对就完事,它必须贯穿 Count、Find、Preload、JOIN 全流程。少一个 Where("tenant_id = ?"),就可能让租户 B 看到租户 A 的数据。


















