postgresql下schema隔离是gorm多租户最可靠方案,因字段隔离易被count/preload/raw/unscoped绕过且context取tenant_id脆弱;mysql仅能依赖字段隔离并需强制约束、索引与封装防护。

PostgreSQL 下 schema 隔离是 GORM 多租户最可靠的选择,字段级隔离(tenant_id)极易因漏加条件或 ORM 方法绕过导致数据越权;MySQL 则只能靠字段隔离 + 强制约束兜底。
为什么 GORM 的 Scopes 和 context.Value 不能作为字段隔离的主力
Scopes 看似方便,但 Count()、Preload()、Raw()、Unscoped() 全部默认不继承——db.Preload("Orders").First(&user) 加载的是全库订单,不是当前租户的。更危险的是:db.Unscoped().Where("id = ?", 123).Find(&u) 直接跳过所有过滤逻辑。
从 context.Context 取 tenant_id 同样脆弱:类型断言失败会 panic;异步任务中 ctx 可能为空;单元测试常 mock DB 层,漏条件 SQL 根本不会暴露。
- 所有 DAO 方法签名必须显式接收
tenantID string参数,禁止封装“自动取租户 ID”工具函数 -
deleted_at软删除字段必须和tenant_id同级约束:WHERE tenant_id = ? AND deleted_at IS NULL - 每张表的
tenant_id字段必须建联合索引,例如(tenant_id, email),否则查询延迟秒级起步
PostgreSQL schema 隔离:必须为每个租户缓存独立 *sql.DB 实例
靠中间件里执行 db.Exec("SET search_path TO tenant_abc") 是无效的——search_path 是 session 级变量,连接归还池后状态丢失,下次复用该连接时仍沿用旧 schema,必然串租户。
真正安全的做法是:用 sync.Map 缓存 tenantID → *sql.DB,首次访问时初始化专属实例:
- DSN 不带
dbname=(PostgreSQL 下可省略),连接后立刻执行db.Exec("SET search_path TO " + safeSchemaName) -
safeSchemaName必须白名单校验,正则建议^[a-z][a-z0-9_]{2,30}$,禁止拼接用户输入 - 每个实例必须调用
db.SetMaxOpenConns(5)和db.SetConnMaxLifetime(5 * time.Minute),防连接数爆炸
GORM 关联查询(Preload / Count)如何强制走租户隔离
哪怕主查询加了 ByTenant(tenantID) scope,Preload("Orders") 也不会透传条件。GORM 不做 scope 继承,这是最常被忽略的越权点。
正确写法只有两种:
- 改用 schema 隔离:所有查询天然落在当前
search_path下,Preload自动生效 - 字段隔离下必须显式传参:
db.Preload("Orders", func(db *gorm.DB) *gorm.DB { return db.Where("tenant_id = ?", tenantID) }) -
Count()同理:db.Where("tenant_id = ?", tenantID).Model(&User{}).Count(&count)
MySQL 下字段隔离是唯一可行路径,但必须补足三道防线
MySQL 没有 search_path,也没有原生 schema 权限隔离能力。“分库”等于每个租户一个 *sql.DB 实例,连接池无法复用,1000 租户 ≈ 1000 个独立连接池,K8s 下健康检查和空闲连接回收全部恶化。
字段隔离必须配合:
- 数据库层强制约束:
ALTER TABLE orders ADD COLUMN tenant_id VARCHAR(32) NOT NULL+CREATE INDEX idx_orders_tenant_id ON orders (tenant_id) - 行级权限替代方案(如视图或应用层全局 WHERE 注入),因为 MySQL 不支持 RLS
- 禁用裸
db.Where(),所有 DAO 封装成FindWithTenant(tenantID, ...)并在内部硬编码WHERE tenant_id = ?
最危险的不是你没写租户条件,而是某次 Raw() 或 Joins() 调用绕过了所有封装——schema 隔离至少从数据库层就切开了边界,这道防线没法被代码绕过。











