GORM不原生支持分库分表,需应用层手动管理多个*gorm.DB实例并按分片键路由;分表依赖动态TableName()回调,事务和迁移须逐库处理。

分库分表不是GORM原生支持的功能
GORM本身不提供分库分表能力,它只负责单个数据库连接上的CRUD和关系映射。所谓“GORM实现分库分表”,实际是靠你在应用层做路由、连接管理、SQL重写或结合中间件完成的。官方文档里找不到 ShardBy 或 UseDatabase 这类方法——这些都得你自己控制。
手动切换DB实例是最直接的分库方式
如果你按租户ID或业务域把数据拆到不同MySQL实例(比如 db_shard_01、db_shard_02),最稳妥的做法是预创建多个 *gorm.DB 实例,运行时根据规则选一个用:
- 每个实例独立调用
gorm.Open(),传入不同DSN - 用 map 或切片缓存这些实例,避免重复初始化
- 路由逻辑写在Service层,不要塞进Model里(否则测试和复用困难)
- 注意事务无法跨实例,
db.Transaction()只对当前实例生效
示例:根据 tenant_id % 4 选库
// 全局变量或依赖注入
var dbShards = make([]*gorm.DB, 4)
func GetTenantDB(tenantID uint) *gorm.DB {
idx := tenantID % 4
return dbShards[idx]
}
分表必须靠动态表名 + 自定义回调
GORM没有“自动分表”机制,但允许你通过 TableName() 方法返回运行时计算的表名。关键点在于:这个方法只影响单次查询,且必须配合 Session(&gorm.Session{DryRun: true}) 等方式绕过GORM的预编译缓存(否则表名不会刷新)。
立即学习“go语言免费学习笔记(深入)”;
- 在Model结构体上实现
TableName() string方法,内部根据字段值(如user_id)算出后缀 - 务必禁用GORM的表名缓存:
db.Session(&gorm.Session{NewDB: true})或显式传入clause.Table - 联合查询(JOIN)、预加载(Preload)会失效,因为GORM不知道关联表也分了
- 迁移(Migrate)需手动遍历所有分表执行,
AutoMigrate默认只作用于TableName()返回的第一个表
示例:按 user_id 分16张订单表
func (Order) TableName() string {
userID := ctx.Value("user_id").(uint)
return fmt.Sprintf("orders_%d", userID%16)
}
别忽略连接池和事务隔离的副作用
分库后,每个DB实例都有独立连接池。如果路由不均,可能某个库的连接被打满,而其他库空闲;更麻烦的是,跨库操作看似用了同一个 *gorm.DB 变量,实则底层连接完全不同。
- 不要在一次HTTP请求中混用多个分库实例做事务——MySQL不支持跨实例XA事务
- 连接池参数(
MaxOpenConns、MaxIdleConns)要按总实例数重新评估,否则容易OOM - 日志和链路追踪需打标分库/分表键,否则排查时根本分不清SQL发到了哪个库
- 如果用了读写分离,分库+读写分离的组合会让路由逻辑爆炸式增长,建议先固化分库策略再加读写
真正难的从来不是怎么写分表逻辑,而是怎么让分片键变更、扩容、数据迁移不中断服务——这部分GORM完全不管,得靠你设计双写、影子库、一致性校验来兜底。


















