gorm 不支持真正的延迟加载,其所谓“延迟关联查询”实为误用 preload 或混淆查询时机所致;preload 是预加载,在主查询后立即通过 join 或 in 子句批量获取关联数据,并非访问时触发。

延迟加载在 GORM 中根本不存在
GORM 本身不支持 MyBatis 或 Hibernate 那种“访问 getter 时触发 SQL”的延迟加载机制。它没有代理对象、不拦截方法调用、也不维护 session 级别的上下文。所谓“GORM 延迟关联查询”,其实是开发者误用 Preload 或混淆了查询时机导致的错觉。
为什么 Preload 不是延迟加载,但常被误用
Preload 是预加载(eager loading),它会在主查询之后立即发起 JOIN 或子查询,把关联数据一次性取回。常见错误是这样写:
db.Preload("Tags").Find(&posts)
这会生成两条 SQL:一条查 posts,一条查 tags(通过 IN 子句批量加载)。但它不是“延迟”,而是“提前合并”。真正想避免 N+1 却没写 Preload,才可能掉进坑里:
- 循环中访问
post.Tags→ 每次触发一次SELECT * FROM tags WHERE post_id = ? - 分页时先
Limit(10).Offset(0).Find(&posts),再对每个post单独查关联 → 实际执行 1 + 10 次查询 - 用
Joins("Tags")但没处理重复行 → 分页总数计算错误(因 JOIN 导致行数膨胀)
分页 + 关联数据的正确组合方式
核心原则:分页必须基于主表逻辑行数,关联数据只能作为附加信息加载,不能参与分页计数。
推荐做法分两步:
- 第一步:用子查询或 CTE 先获取分页后的主键 ID 列表(例如
SELECT id FROM posts ORDER BY created_at DESC LIMIT 10 OFFSET 0) - 第二步:用这些 ID 批量查主表完整数据 + 关联表(
Preload或显式Joins)
GORM v1.25+ 支持更简洁的写法:
var postIDs []uint
db.Model(&Post{}).Select("id").Order("created_at DESC").Limit(10).Offset(0).Scan(&postIDs)
<p>var posts []Post
db.Preload("Tags").Where("id IN ?", postIDs).Find(&posts)</p>
注意:Preload 在第二步中才生效,且只加载已确定的 10 条记录的关联数据,彻底规避 N+1 和 JOIN 分页错乱问题。
容易被忽略的性能陷阱
很多人以为加了 Preload 就万事大吉,但实际还有三个硬伤:
-
Preload默认用 IN 查询,当关联数据量极大(如一篇博文有上万标签),IN 子句可能超长或触发 MySQL 的max_allowed_packet限制 - 多级嵌套
Preload("Author.Profile.Avatar")会生成多个独立子查询,而不是单次 JOIN,IO 次数仍可能上升 - 如果关联表缺少复合索引(比如
post_tag表只建了post_id索引,没建(post_id, tag_id)联合索引),Preload的 IN 查询会变慢
真正要压测验证的,不是“有没有用 Preload”,而是“分页后那 10 条记录,关联数据加载耗时是否稳定在 5ms 内”。否则优化只是幻觉。











