gorm分页本身安全,串租户主因是连接未与租户严格绑定:需用sync.map缓存租户专属*sql.db(初始化时set search_path并限流),分页前必须取该实例,禁用全局db或中间件临时设schema。

PostgreSQL schema 隔离下,GORM 的 Limit/Offset 分页本身不越权,但分页逻辑若漏掉租户绑定或跨 schema 复用连接,就会查错数据——这不是分页的问题,而是连接归属没管住。
为什么 GORM 分页在 schema 隔离下仍会串租户
schema 隔离靠 SET search_path TO tenant_abc 生效,而这个设置只对当前连接的 session 有效。如果分页请求用了被缓存但未初始化 search_path 的 *sql.DB 实例,或者多个租户共享了同一个未隔离的连接池,db.Limit(20).Offset(40).Find(&users) 就可能落在 public 或其他租户 schema 上。
常见错误现象:
- 同一页面刷新几次,偶尔返回其他租户的用户列表
- 后台定时任务跑分页统计,结果混入非目标租户数据
- 使用
db.Session(&gorm.Session{DryRun: true})查 SQL 时正常,真实执行却错乱——因为 DryRun 不走真实连接
根本原因不是分页写法错,而是连接没和租户严格一对一绑定。GORM 自己不管理 search_path,它只负责发 SQL;谁提供连接,谁就得保证这个连接“只属于该租户”。
分页前必须确保租户专属 *sql.DB 已就绪
不能在 handler 里临时调 db.Exec("SET search_path..."),也不能复用全局 gorm.DB。分页操作前,必须从缓存中取出已初始化好的租户专属实例:
- 用
sync.Map缓存:key 是租户 ID(如"acme"),value 是已执行过db.Exec("SET search_path TO acme")的*sql.DB - 首次获取时 lazy 初始化:打开连接 → 立即设
search_path→ 调db.SetMaxOpenConns(5)和db.SetConnMaxLifetime(5 * time.Minute)→ 存入 map - 白名单校验 schema 名:正则匹配
^[a-z][a-z0-9_]{2,30}$,禁止拼接任意用户输入 - 别在中间件里“统一设 search_path”,那只是给当前 goroutine 的连接设,无法保证后续 Query 复用的是同一连接
GORM 分页调用本身无需改写,但必须走对连接
Limit、Offset、Count 这些方法本身是安全的——只要底层连接已绑定正确 schema。你不需要重写分页逻辑,也不用给每个分页加 Where("tenant_id = ?")(那是字段隔离才要干的事)。
正确做法示例:
// ✅ 正确:先取租户专属 db,再分页 tenantDB := GetTenantDB(tenantID) // 返回 *gorm.DB,底层 *sql.DB 已设好 search_path var users []User err := tenantDB.Limit(20).Offset(40).Find(&users).Error <p>// ❌ 错误:用全局 db,哪怕加了 Scopes 也无效 globalDB.Scopes(ByTenant(tenantID)).Limit(20).Offset(40).Find(&users) // ByTenant 只对 WHERE 有用,不影响 schema,且 Preload/Count 仍绕过它 </p>
注意:Preload 关联查询也自动落在当前 schema,无需额外处理;但如果你手动写了 Raw() 查询,必须确认 SQL 中没硬编码 schema 名(如 "public.users"),否则会绕过 search_path。
分页性能与索引无关 schema,但联合索引仍需保留
schema 隔离下,每张表物理独立,tenant_acme.users 和 tenant_bob.users 互不影响。所以分页慢,不是因为跨租户扫描,而是单租户内数据量大 + 缺少合适索引。
仍需为每张表建联合索引,例如:
-
(tenant_id, created_at)—— 支持按时间分页 -
(tenant_id, status, updated_at)—— 支持带状态筛选的分页
虽然 tenant_id 在 schema 隔离中不参与 WHERE 过滤,但很多业务代码仍保留该字段用于审计、迁移或混合部署过渡,索引能避免隐式全表扫描。另外,PostgreSQL 的 pg_stat_all_tables 按 schema 统计,缺失索引问题更容易被监控捕获。
最易被忽略的一点:分页接口的缓存 key 必须包含租户标识,比如 tenant:acme:user:list:page=3:size=20。用通用 key(如 user:list:page=3)会导致一个租户的分页结果被另一个租户命中,这不是数据泄露,而是数据错乱——而且很难复现和定位。











