gorm 的 page 不支持多数据源自动路由,因其分页操作(如 limit/offset)必须绑定具体 *gorm.db 实例,无法跨数据源复用同一套逻辑;需通过 scopes 解耦分页参数并显式指定数据源,或使用如 goweb3 的 pagedao 通过 dbmeta 显式声明数据源配置来适配。

为什么 GORM 的 Page 不支持多数据源自动路由
GORM 本身没有内置的「多数据源分页」抽象,Page 相关操作(如 Limit/Offset)必须绑定到具体 *gorm.DB 实例上。当你有 MySQL 和 PostgreSQL 两个数据源时,db1.Offset(0).Limit(10).Find(&list) 和 db2.Offset(0).Limit(10).Find(&list) 是完全独立的调用,无法共用同一套分页逻辑——除非你把分页参数和查询构建过程从 DB 实例中解耦出来。
用 Scopes 封装分页逻辑,但必须传入 *gorm.DB
不能写成全局函数直接返回 []T,否则就丢失了数据源上下文。正确做法是让每个 scope 接收并返回 *gorm.DB,再由调用方决定用哪个 DB 实例启动链式调用:
func PageScope(page, pageSize int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if page // 使用示例:切换不同数据源只需换 db 实例
db1.Scopes(StatusScope("active"), PageScope(2, 15)).Find(&users) // MySQL
db2.Scopes(DateRangeScope("2025-01-01", ""), PageScope(2, 15)).Find(&logs) // PostgreSQL
- 所有条件 scope(如
StatusScope、DateRangeScope)都应设计为纯函数,不依赖任何全局 DB 变量 -
PageScope内部不做数据库类型判断,因为 LIMIT/OFFSET 在 MySQL/PostgreSQL 中语义一致;但 Oracle 需要 ROWNUM,此时应改用PaginationInnerInterceptor类似机制或显式分支 - 避免在 scope 里调用
Count()—— 总数查询需单独执行,且必须复用相同条件链
总数查询必须与分页查询共享完全相同的 WHERE 条件
很多人在分页时分别写 db.Where(...).Count(&total) 和 db.Where(...).Offset().Limit().Find(),看似一样,实则容易因 scope 执行顺序、指针传递或中间修改导致条件不一致。最稳妥的方式是先构建好条件链,再复用:
baseQuery := db.Scopes(StatusScope(status), DateRangeScope(start, end)) var total int64 baseQuery.Count(&total) <p>var list []User baseQuery.Scopes(PageScope(page, size)).Find(&list) </p>
- 不要用
db.Session(&gorm.Session{NewDB: true})或临时 clone,GORM 的 scope 是函数式组合,直接复用 baseQuery 最可靠 - 如果使用 goweb3 的
pagedb模块,它内部已通过pagedbrequest.PageRequest统一管理条件与分页参数,但底层仍依赖各数据源的*gorm.DB实例注入,不可跳过数据源选择步骤 - ClickHouse 场景下注意:OFFSET 越大性能越差,
PageScope应配合时间戳游标(如WHERE created_at > ? ORDER BY created_at LIMIT 10)替代传统分页
goweb3 的 PageDao 如何适配多数据源
goweb3 的 PageDao 本质是泛型封装,其 QueryModel 方法接收一个 dbmeta.DataSource 参数,该参数指向具体数据源配置(如 "mysql-primary" 或 "pg-read")。它不是自动路由,而是显式声明:
dao := NewPageDao[User, uint](dbmeta.GetDataSource("pg-read"))
result, err := dao.QueryModel(ctx, &PageRequest{
Page: 3,
Size: 20,
Filters: map[string]interface{}{"status": "published"},
})
- 每个数据源需提前注册到
dbmeta,否则GetDataSource返回 nil -
PageDao内部会根据数据源类型(MySQL/PostgreSQL/ClickHouse)自动选择分页方言,但不会跨数据源合并结果;多源聚合分页需业务层自行实现 - 软删除字段(如
deleted_at)是否生效,取决于对应数据源的 GORM 配置是否启用SoftDelete,不同源可异构配置
多数据源分页真正的难点不在语法,而在条件一致性与总数准确性。只要 scope 链可复用、总数与列表共用同一查询基线、数据源选择显式可控,就避开了 90% 的坑。其余细节,比如 ClickHouse 的偏移限制或 Oracle 的 ROWNUM 重写,都是可插拔的方言适配问题,不必强求统一抽象。











