Beego 不支持 ORM 自动添加 tenant_id 条件,需开发者手动在所有查询中显式过滤;PostgreSQL 可通过租户专属 *sql.DB 注入 search_path 实现 schema 隔离,MySQL 则必须采用字段隔离加全链路校验;文件、配置、缓存、日志等均须按租户前缀隔离。

Beego 里不能靠 ORM 自动加 tenant_id 条件
Beego 的 orm.QuerySeter 没有类似 MyBatis-Plus 多租户插件或 GORM Scopes 那样的内建租户拦截机制。你写 o.QueryTable("user").Filter("id", 123),它不会自动补 tenant_id = ? ——漏写就是跨租户读取。这不是 Bug,是设计使然:Beego 把数据隔离责任完全交给开发者。
常见错误现象:
- API 返回了其他租户的用户列表,日志里查不到 WHERE tenant_id 的痕迹
-
o.LoadRelated(&u, "Orders")加载关联数据时,Orders表没带租户条件,结果混入了别家订单 - 用
o.Raw()执行统计 SQL,忘记手动拼AND tenant_id = ?,报表数据全错
正确做法是封装基础 DAO 层:
- 所有查询方法签名必须显式接收
tenantID string参数 - 每个
QueryTable调用后立刻链式调用.Filter("tenant_id", tenantID),不接受裸调用 - 关联加载必须手动控制:先查主表带租户条件,再用
o.QueryTable("order").Filter("user_id__in", ids).Filter("tenant_id", tenantID) - 禁用全局
orm.RegisterModel后直接操作表名;改用o.QueryTable("tenant_" + tenantID + "_user")这种方式属于反模式,破坏可维护性
PostgreSQL schema 隔离在 Beego 中怎么落地
Beego 原生不支持运行时切换 PostgreSQL search_path,但你可以绕过 ORM 层,在连接初始化阶段注入。关键不是“让 Beego 支持”,而是“让每个租户请求拿到的 *sql.DB 实例已绑定好 schema”。
实现要点:
- 用
sync.Map缓存tenantID → *sql.DB,避免每次请求新建连接 - 每个租户专属
*sql.DB的 DSN 必须带上 URL 编码参数:options=-c+search_path%3Dtenant_abc - Beego 的
orm.RegisterDataBase只能注册固定名称的数据源(如default),所以你要放弃用orm管理多租户连接,改用原生sql.Open+ 手动传参给 DAO - 事务必须限定在单个租户连接内:不能在一个事务里混用两个租户的
*sql.DB实例,否则search_path生效范围错乱
示例伪代码:
db, _ := sql.Open("postgres", "user=... dbname=saas options=-c+search_path%3Dtenant_abc")
db.SetMaxOpenConns(5)
tenantDBs.Store("abc", db)
// 后续 DAO 方法里:
db, _ := tenantDBs.Load(tenantID)
db.QueryRow("SELECT name FROM users WHERE id = $1", 123)
MySQL 下强行 schema 隔离等于自废武功
MySQL 没有 search_path,也没有真正的 schema 级权限隔离能力。“为每个租户建一个 database”看似隔离,但在 Beego 场景下会迅速失控。
问题根源:
- Beego 的
orm.RegisterDataBase不支持动态注册;你没法在请求中实时注册tenant_123这类数据库名 - 每个 database 对应一个独立
*sql.DB实例,连接池互不共享;1000 租户 × 默认 100 连接 = 10 万并发连接,MySQL 服务端直接拒绝新连接 - K8s 环境下,每个
*sql.DB都要单独做健康检查、空闲连接回收,OOM 和连接泄漏风险指数级放大
如果你非要用 MySQL,唯一现实路径是字段隔离 + 全链路强校验:
- 所有 HTTP 请求拦截器中解析
X-Tenant-ID并写入context.Context - 每个 DAO 方法开头校验
tenantID != "",为空直接返回 400 - 所有 SQL 构建处强制拼
WHERE tenant_id = ?,禁止任何裸QueryTable().Filter() - 用
o.Raw("SELECT ... FROM orders WHERE tenant_id = ? AND id = ?").Params(tenantID, id)替代模型查询,杜绝 ORM 魔法带来的不确定性
文件与配置也要按租户切分路径和 key
Beego 的 beego.AppConfig 是全局单例,beego.BConfig.AppPath 是静态路径 —— 它们都不感知租户。这意味着如果你把上传文件存到 uploads/xxx.png,所有租户会写到同一个目录;如果从 viper 或 etcd 读配置,不加租户前缀,就会拿到别人的数据。
必须手动隔离:
- 文件路径第一级必须是租户 ID:
filepath.Join(uploadRoot, tenantID, "uploads", uuid.New().String()+".png") - 写入前调用
filepath.Clean()并检查是否仍以uploadRoot + "/" + tenantID开头,拦截../路径遍历 - 配置中心 key 命名强制前缀:
tenant:abc:feature:enable_ai,而非feature:enable_ai - Beego 日志文件也需按租户分目录,否则审计时无法定位某租户的行为日志
最容易被忽略的一点:Beego 的 Cache 默认也是全局的。如果你用 beego.Cache.Put("user_list", data, 60),这个 key 对所有租户可见。必须改用带租户前缀的 key:beego.Cache.Put("tenant_"+tenantID+"_user_list", data, 60)。











