Revel 框架虽无内置多租户支持,但可通过 filter 识别租户(如从 Host 提取子域名)、注入 GORM 实例到上下文,并在模型层显式传递 tenantID 来实现安全隔离。

Revel 框架本身不内置多租户支持,但它的 filter 机制、上下文传递能力和模块化设计,让多租户可以干净地落地——关键不是“能不能”,而是“在哪识别租户”和“怎么把租户上下文传到 GORM 层”。
租户识别必须放在 filter 中,且优先级要高
Revel 的请求生命周期里,filter 是唯一能统一拦截所有 HTTP 请求并注入上下文的地方。如果在 controller 或 model 里临时解析 Host 或 header,会导致逻辑散落、漏覆盖、测试困难。
- 必须用
revel.BEFORE级别注册 filter,确保它在任何 controller 执行前就运行 - 推荐从
request.Headers.Get("Host")提取子域名(如acme.example.com→acme),比依赖X-Tenant-ID更符合 SaaS 用户体验 - 解析失败时应直接返回
400 Bad Request或跳转默认页,不要把空租户 ID 传下去 - 避免在 filter 里做数据库查询或远程调用——租户识别阶段只做轻量字符串处理
GORM 连接不能全局单例,必须按请求绑定
GORM v2 的 *gorm.DB 是线程安全的,但它不支持运行时切换底层连接池。若强行复用一个全局 DB 并动态改 Config.DSN,会引发连接混乱、事务失效、prepared statement 错误等严重问题。
- 启动时预建多个
*gorm.DB实例(如DBShard1,DBShard2),每个对应一个租户或租户组 - 在 filter 中识别出租户 ID 后,查表或映射得到对应 DB 实例,再用
revel.Context.Set("db", dbInstance)注入上下文 - controller 中通过
c.Params.Get("db").(*gorm.DB)取出,而不是调用全局变量 - 注意:GORM 的
Session或WithContext不足以隔离租户,它们只影响单次查询,不改变底层连接池归属
模型层需适配租户上下文,不能硬编码表名或条件
即使连接已路由正确,如果模型方法里写死 Where("tenant_id = ?", "acme") 或拼接表名,就会破坏可维护性,也容易在关联查询中漏掉租户约束。
- 定义一个基础模型结构体,内嵌
tenantID string字段,并在BeforeCreate回调中自动填充 - 所有查询入口(如
FindOrdersByUser)显式接收tenantID参数,禁止从全局变量读取 - 避免使用 GORM 的
Table()动态切表(如db.Table("orders_" + tenantID)),这会让迁移、索引、ORM 缓存全部失效 - 如果采用共享库+独立 Schema 方案(如 PostgreSQL),可在连接 DSN 中直接指定
search_path=acme,由数据库层完成隔离
最容易被忽略的是租户上下文的生命周期管理——filter 设置的 db 实例只在当前 HTTP 请求内有效,但如果你在 goroutine 中异步调用模型方法(比如发邮件、写日志),revel.Context 已不可用。这种场景必须显式把 *gorm.DB 作为参数传入,不能依赖上下文隐式传递。











