全部学科
NodeJS全栈
nodejs
Python全栈
python
小程序首页
📅 2026-05-21 8 分钟 ✍️ juanwangdev

GORM 多租户数据隔离

GORM 多租户隔离通过 Statement 回调自动注入 tenant_id 条件,确保查询、写入操作限定在当前租户范围内。

多租户隔离机制

租户模型定义

Go
type TenantModel struct {
    ID        uint `gorm:"primaryKey"`
    TenantID  uint `gorm:"index;not null"` // 租户标识
    Name      string
    CreatedAt time.Time
}

查询自动过滤

Before Query 回调注入租户条件

Go
func (db *gorm.DB) SetupTenantFilter(tenantID uint) *gorm.DB {
    return db.Session(&gorm.Session{
        SkipHooks: false,
    }).Callback().Query().Before("gorm:query").Register("tenant:query", func(db *gorm.DB) {
        db.Where("tenant_id = ?", tenantID)
    })
}

// 使用
db = db.SetupTenantFilter(currentTenantID)
var orders []Order
db.Find(&orders) // 自动注入 WHERE tenant_id = ?

写入自动注入

Before Create 回调注入租户 ID

Go
func SetTenantID(tenantID uint) func(db *gorm.DB) {
    return func(db *gorm.DB) {
        db.Statement.SetColumn("tenant_id", tenantID)
    }
}

func (db *gorm.DB) SetupTenantWrite(tenantID uint) *gorm.DB {
    return db.Callback().Create().Before("gorm:create").Register("tenant:create", func(db *gorm.DB) {
        // 检查是否已设置 tenant_id
        if _, ok := db.Statement.Dest.(map[string]interface{}); ok {
            if v, ok := db.Statement.Dest.(map[string]interface{})["tenant_id"]; !ok || v == nil {
                db.Statement.SetColumn("tenant_id", tenantID)
            }
        }
    })
}

// 使用
db = db.SetupTenantWrite(currentTenantID)
db.Create(&Order{Name: "test"}) // 自动注入 tenant_id

Scopes 封装租户上下文

统一租户作用域

Go
func TenantScope(tenantID uint) func(db *gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        return db.Where("tenant_id = ?", tenantID)
    }
}

// 查询使用
db.Scopes(TenantScope(currentTenantID)).Find(&orders)

// 更新使用
db.Scopes(TenantScope(currentTenantID)).Model(&Order{}).Where("id = ?", id).Update("name", "new")

注意: Scopes 需手动调用,适合不依赖全局回调的场景。

删除与更新保护

防止跨租户操作

Go
func (db *gorm.DB) ProtectTenant(tenantID uint) *gorm.DB {
    return db.Callback().Update().Before("gorm:update").Register("tenant:update", func(db *gorm.DB) {
        if _, ok := db.Statement.Clauses["WHERE"]; !ok {
            db.AddError(errors.New("更新操作必须带 WHERE 条件"))
            return
        }
        db.Where("tenant_id = ?", tenantID)
    })
}

// 删除保护
func (db *gorm.DB) ProtectTenantDelete(tenantID uint) *gorm.DB {
    return db.Callback().Delete().Before("gorm:delete").Register("tenant:delete", func(db *gorm.DB) {
        db.Where("tenant_id = ?", tenantID)
    })
}

上下文传递租户 ID

Gin 中间件示例

Go
func TenantMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        tenantIDStr := c.GetHeader("X-Tenant-ID")
        tenantID, err := strconv.ParseUint(tenantIDStr, 10, 64)
        if err != nil {
            c.AbortWithStatusJSON(400, gin.H{"error": "invalid tenant id"})
            return
        }
        
        // 存入上下文
        ctx := context.WithValue(c.Request.Context(), "tenant_id", tenantID)
        c.Request = c.Request.WithContext(ctx)
        c.Next()
    }
}

// 从上下文获取租户 ID
func GetTenantFromContext(c *gin.Context) uint {
    tenantID, _ := c.Request.Context().Value("tenant_id").(uint)
    return tenantID
}

注意: 严禁将租户 ID 硬编码或依赖用户输入,应从认证令牌、请求头或上下文获取。

要点总结

  • 通过回调机制在查询、创建、更新、删除时自动注入 tenant_id 条件
  • 写入操作自动填充租户 ID,查询操作自动追加租户过滤
  • Scopes 提供手动租户作用域方案,适合灵活场景
  • 删除和更新操作必须强制带 WHERE 条件,防止全表误操作
  • 租户 ID 应从认证上下文获取,禁止依赖用户输入

存放路径:D:\git2\jwdev\articles\GORM\专家\分库分表与多租户\多租户数据隔离.md

📝 发现内容有误?点击此处直接编辑

← 上一篇 GORM 分表策略实现
下一篇 → GORM 跨库事务处理
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库