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
📝 发现内容有误?点击此处直接编辑