全部学科
Python全栈
python
NodeJS全栈
nodejs
📅 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 跨库事务处理
扫码体验小程序
加载中
想在手机上刷题学习?
使用微信卷王开发者小程序,打开首页顶部扫码功能识别二维码