
GORM 的 Joins() 方法仅执行 SQL JOIN 查询,但不会自动将关联数据填充到结构体字段中;必须配合 Preload() 或手动 SQL 构建 + 结构体映射才能实现关联数据加载。
gorm 的 `joins()` 方法仅执行 sql join 查询,但不会自动将关联数据填充到结构体字段中;必须配合 `preload()` 或手动 sql 构建 + 结构体映射才能实现关联数据加载。
在 GORM 中,Joins() 是一个纯查询优化手段,它只影响生成的 SQL 语句(如 INNER JOIN),而完全不负责结果集到 Go 结构体的嵌套映射。这意味着即使你在 SELECT 中通过 JOIN 获取了 languages 或 movies 的字段,GORM 默认仍会将 Artist.Movies 和 Movie.Language 视为未初始化的空切片或零值结构体——这正是你观察到 Movies 始终为空数组的根本原因。
✅ 正确做法一:优先使用 Preload()(推荐)
Preload() 是 GORM 官方支持的预加载机制,专为解决 N+1 问题而设计,语义清晰、类型安全,且能正确填充嵌套关系:
var artists []Artist
err := db.Preload("Movies").Preload("Movies.Language").Where("artists.id IN (?)",
db.Table("artist_movies").
Select("artist_id").
Where("movie_id IN (?)",
db.Table("movies").Select("id").Where("language_id = ?", langID),
),
).Find(&artists).Error若需按语言名称过滤(如 "tamil"),可先查出对应 Language.ID,再组合查询:
var lang Language
db.Where("name = ?", "tamil").First(&lang)
var artists []Artist
db.Preload("Movies").Preload("Movies.Language").
Joins("JOIN artist_movies ON artist_movies.artist_id = artists.id").
Joins("JOIN movies ON movies.id = artist_movies.movie_id").
Where("movies.language_id = ?", lang.ID).
Group("artists.id").
Find(&artists)⚠️ 注意:
Preload()会发起额外的 SQL 查询(非单条 JOIN),但 GORM 会自动去重并批量加载,性能通常优于手写 JOIN + 手动映射,且开发体验更可靠。
✅ 正确做法二:手动 JOIN + 自定义结构体映射(高级场景)
当必须用单条 JOIN SQL(例如复杂聚合、性能极致要求),则需绕过 GORM 的自动映射,改用 Raw() 或 Scan() 配合扁平化结构体:
type ArtistWithMovies struct {
ArtistID uint
ArtistName string
MovieID uint
MovieTitle string
LangID uint
LangName string
}
var results []ArtistWithMovies
err := db.Table("artists").
Select("artists.id as artist_id, artists.name as artist_name, "+
"movies.id as movie_id, movies.title as movie_title, "+
"languages.id as lang_id, languages.name as lang_name").
Joins("JOIN artist_movies ON artist_movies.artist_id = artists.id").
Joins("JOIN movies ON movies.id = artist_movies.movie_id").
Joins("JOIN languages ON languages.id = movies.language_id").
Where("languages.name = ?", "tamil").
Scan(&results).Error随后在 Go 层手动聚合为嵌套结构:
artistMap := make(map[uint]*Artist)
for _, r := range results {
if _, exists := artistMap[r.ArtistID]; !exists {
artistMap[r.ArtistID] = &Artist{ID: r.ArtistID, Name: r.ArtistName}
}
artistMap[r.ArtistID].Movies = append(artistMap[r.ArtistID].Movies,
Movie{ID: r.MovieID, Title: r.MovieTitle, Language: Language{ID: r.LangID, Name: r.LangName}})
}
artists := make([]Artist, 0, len(artistMap))
for _, a := range artistMap {
artists = append(artists, *a)
}❌ 常见误区与总结
-
误区:认为
Joins()+Find(&[]Struct)能自动填充has-many或belongs-to字段 → ❌ 错误。GORM 永远不会基于 JOIN 结果自动构造嵌套对象。 -
误区:混用
Joins()和Preload()在同一查询中 → ❌ GORM v1 不支持(Issue #1436 明确说明),会导致Preload()被忽略。 -
关键原则:
- 若追求开发效率与可维护性 → 用
Preload()(支持链式、条件过滤、深度嵌套); - 若追求极致单查询控制 → 放弃 GORM 自动映射,用
Raw()/Scan()+ 手动聚合; - 永远不要依赖
Joins()来“填充”结构体关联字段。
- 若追求开发效率与可维护性 → 用
最后提醒:示例代码中 Movie.Language 定义为值类型(Language 而非 *Language),虽语法合法,但易引发意外拷贝;建议统一使用指针(*Language, []*Movie)以明确引用语义,并与 GORM 关联标签保持一致。

















