不能用一个*gorm.DB变量切换数据源,因其绑定独立连接池、回调链等,重复赋值导致连接泄漏、事务失效、查询错库;分页须按数据源单独处理,总数统计需新建session,Offset须在Limit前调用,且需硬限制页大小、校验参数合法性。

多数据源切换和分页不能混在同一个 *gorm.DB 实例上做——前者要求多个独立连接池,后者要求每个查询链路隔离且可复用。强行共用会导致事务失效、总数统计错乱、分页偏移错位。
为什么不能用一个 *gorm.DB 变量切换数据源
GORM 的 *gorm.DB 不是轻量上下文,它内部绑定了 *sql.DB 连接池、回调链、日志器、插件和配置。调用 gorm.Open 两次但赋值给同一个变量(比如 db = gorm.Open(dsn1) 再 db = gorm.Open(dsn2)),前一次的连接池不会自动关闭,造成泄漏;后续所有 db.Transaction() 或 db.Preload() 都会作用在错误的库上。
常见错误现象:
- 第一次查
userDB.Find(&u)正常,第二次用同一变量连logDB却返回 “invalid connection” -
userDB.AutoMigrate()把表建到了日志库,因为中间漏了db = userDB赋值 - 驱动导入漏下划线:如
import "gorm.io/driver/mysql"少了_,gorm.Open不报错,但首次Find卡死或报"pq: password authentication failed"
分页必须按数据源单独处理
每个 *gorm.DB 实例的分页逻辑要独立初始化、独立校验、独立执行。共用 Offset/Limit 链式调用时,若未重置条件,Where 和 Order 会残留污染下一个查询。
实操要点:
-
Offset必须在Limit前调用才稳定(虽 GORM v2 允许互换,但跨库场景下顺序不一致易出错) - 总数查询绝不能复用主查询链:
db.Where("status = ?", 1).Offset(20).Limit(10).Count(&total)返回的是 10,不是符合条件的总条数;正确做法是新建 session:db.Session(&gorm.Session{NewDB: true}).Model(&User{}).Where("status = ?", 1).Count(&total) - 每页大小必须硬限制(如 ≤100),
page_size=9999直接导致慢查询甚至 DB 拒绝服务 - 前端传
page=abc时,c.ShouldBindQuery(&p)会返回 error,应直接 400,别 fallback 到默认值
运行时路由到对应 DB + 安全分页封装
租户或分片标识必须在入口中间件提取(如从 r.Header.Get("X-Tenant-ID") 或 JWT claim),查主库确认存在后才继续;缓存用 sync.Map 或带 TTL 的内存缓存(如 gcache),key 是租户 ID,value 是已 Ping() 验证过的 *gorm.DB。
handler 中取 DB 并分页示例:
func ListUsers(c *gin.Context) {
tenantID := c.GetString("tenant_id")
db, ok := getTenantDB(tenantID)
if !ok {
c.JSON(404, gin.H{"error": "tenant not found"})
return
}
<pre class="brush:php;toolbar:false;">var p PaginationRequest
if err := c.ShouldBindQuery(&p); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
var users []User
// 分页必须显式 Order,否则结果不稳定
result := db.Order("id ASC").
Offset((p.Page - 1) * p.Limit).
Limit(p.Limit).
Find(&users)
if result.Error != nil {
c.JSON(500, gin.H{"error": result.Error.Error()})
return
}
var total int64
db.Session(&gorm.Session{NewDB: true}).
Model(&User{}).
Count(&total)
c.JSON(200, PaginatedResponse{
Data: users,
Total: total,
Page: p.Page,
Limit: p.Limit,
TotalPages: int((total + int64(p.Limit) - 1) / int64(p.Limit)),
})}
注意:游标分页(基于 id > ?)比 Offset 更适合高频写入场景,但它无法跳页——这点容易被忽略,尤其当产品提“跳转到第 100 页”需求时,仍得回退到传统分页并接受性能代价。


















