分表中间件必须接管SQL解析和表名重写,因原生GORM不解析SQL,db.Table()仅临时覆盖表名,无法实现逻辑表到物理表的动态映射;需在BeforePrepare阶段用AST解析器提取并替换表名,且分片键必须显式传入或从SQL中提取,禁止静默fallback。

分表中间件必须接管 SQL 解析和表名重写
原生 GORM 不解析 SQL,db.Table("orders_01") 只是临时覆盖当前 query 的表名,不改变模型绑定或 schema 缓存。真要实现“逻辑表 orders → 物理表 orders_01/orders_02”,你得在 *gorm.DB 的 Conn 层拦截原始 SQL,用 AST 解析器(如 vitess/sqlparser)提取 FROM 和 INSERT INTO 后的表名,再根据分片键值动态替换。
常见错误现象:db.Model(&Order{}).Where("user_id = ?", 123).Create(&o) 仍发往 orders 表,报错 Table 'db.orders' doesn't exist;或者用了 TableName() 导致所有查询都落到 orders_00,缓存污染。
- 不能依赖
Scope或Session做全局分表——它们只影响单次调用,无法支撑 Raw SQL、事务内多语句等场景 - SQL 解析必须在
Callbacks的BeforePrepare阶段介入,早于预编译;晚了就进不了连接池 - UPDATE/DELETE 的 WHERE 条件里若不含分片键(如
status = 'pending'),应拒绝执行并返回sharding.ErrMissingShardingKey
分片键提取不能靠反射或 Hook 猜
GORM 的 BeforeQuery、AfterFind 等钩子拿不到原始 SQL 中的 WHERE 字段值,也无法安全修改表名。你不能在钩子里写 if v, ok := db.Statement.Clauses["WHERE"].Expression.(clause.Where); ok { ... } —— 这个 Expression 是已编译的占位符结构,不是原始条件字符串。
真正可行的路径只有两条:
- 强制业务层传入分片键:所有
Create/First/Update必须显式带user_id字段,中间件从db.Statement.Dest结构体反射读取(需提前约定字段名) - Raw SQL 场景下要求 SQL 中显式包含
WHERE user_id = ?,中间件从db.Statement.SQL.String()提取参数位置并绑定值 - 禁止自动 fallback:没找到分片键就 panic,别静默路由到默认表——这会导致数据错乱且难以排查
自增主键生成必须脱离数据库 auto_increment
MySQL 的 LAST_INSERT_ID() 在分表场景下完全失效:每个物理表有自己的自增序列,orders_01 插入后返回 1,orders_02 插入也返回 1,冲突必然发生。社区中间件如 gorm.io/sharding 提供 PKSnowflake,但它的 index 参数硬编码为 1024,一旦分片数超限(比如按年分表到 orders_2025),就会 panic。
正确做法是把 ID 生成完全前置:
- 用 Snowflake 改造:把分片 ID 映射为机器 ID 或数据中心 ID 段,避免 runtime 计算 index
- 插入前生成 ID:在
BeforeCreate钩子里调用分布式 ID 服务(如 etcd + lease),再赋值给Order.ID - 禁止任何依赖表级自增的方案——包括
PKMySQLSequence类实现,它本质还是串行 UPDATE + SELECT,高并发下锁表
跨分片聚合查询必须放弃“透明”幻想
所谓“对业务无感的分表中间件”,在 SELECT COUNT(*) FROM orders 这种语句上根本不存在。你没法让中间件自动拆成 64 个 COUNT(*) FROM orders_00 到 orders_63 再合并——因为 GORM 的 Count() 返回的是 int64,不是结果集,中间件无处注入聚合逻辑。
能做的只有明确边界:
- 单分片操作(带分片键的 CRUD)可封装成透明 API
- 跨分片 COUNT / SUM / AVG 必须由业务层显式调用
MultiShardCount(dbShards, "orders", "status = 'paid'")这类函数 - JOIN 操作直接禁止:GORM 的
Preload和Joins只作用于当前*gorm.DB实例,跨库会报ERROR 1146 (42S02): Table 'shard_b.users' doesn't exist - 分页一律用游标(cursor-based),不用
LIMIT OFFSET—— 后者在各分片取 TOP-N 再合并,结果错乱且性能崩塌
最易被忽略的一点:事务隔离级别在分片间无法保证。哪怕你用 db.Transaction() 包住两个分片的写操作,底层仍是两个独立 MySQL 事务,崩溃时只能靠对账表或 MQ 补偿——这不是中间件能绕过去的物理限制。


















