关联预加载是优化关联查询的关键。理解不同预加载策略的特点,选择合适的方式,能显著提升性能。
不预加载时,关联字段是空的:
var user User
db.First(&user, 1)
fmt.Println(user.Roles)
Roles 是空切片,因为 GORM 不会自动加载关联。
var user User
db.Preload("Roles").First(&user, 1)
fmt.Println(user.Roles)
执行两条 SQL:
加载多层关联:
type User struct {
ID uint
Roles []Role
}
type Role struct {
ID uint
Permissions []Permission
}
db.Preload("Roles.Permissions").First(&user, 1)
执行三条 SQL:
同时加载多个关联:
type User struct {
ID uint
Profile Profile
Articles []Article
Roles []Role
}
db.Preload("Profile").
Preload("Articles").
Preload("Roles").
First(&user, 1)
预加载时加条件:
db.Preload("Articles", "status = ?", "published").First(&user, 1)
只加载已发布的文章。
更复杂的条件:
db.Preload("Articles", func(db *gorm.DB) *gorm.DB {
return db.Where("status = ?", "published").
Order("created_at desc").
Limit(10)
}).First(&user, 1)
加载最近 10 篇已发布文章。
用 JOIN 方式预加载:
db.Joins("Profile").First(&user, 1)
只执行一条 SQL,通过 JOIN 获取关联。
适合一对一关联,一对多可能导致重复。
Preload
优点:
缺点:
Joins
优点:
缺点:
加载所有关联:
import "gorm.io/gorm/clause"
db.Preload(clause.Associations).First(&user, 1)
db.Preload("Roles").
Preload("Roles.Permissions").
Preload(clause.Associations).
First(&user, 1)
根据条件动态预加载:
func GetUser(db *gorm.DB, id uint, withProfile, withArticles bool) (*User, error) {
query := db.Model(&User{})
if withProfile {
query = query.Preload("Profile")
}
if withArticles {
query = query.Preload("Articles")
}
var user User
err := query.First(&user, id).Error
return &user, err
}
只加载需要的字段
db.Preload("Articles", func(db *gorm.DB) *gorm.DB {
return db.Select("id, user_id, title")
}).First(&user, 1)
分批预加载
大量关联时分批加载:
var users []User
db.Find(&users)
userIDs := make([]uint, len(users))
for i, u := range users {
userIDs[i] = u.ID
}
var articles []Article
db.Where("user_id IN ?", userIDs).Limit(1000).Find(&articles)
延迟加载
不确定是否需要关联时,延迟加载:
func (u *User) LoadArticles(db *gorm.DB) error {
if u.Articles == nil {
return db.Where("user_id = ?", u.ID).Find(&u.Articles).Error
}
return nil
}
分页查询时预加载:
var users []User
var total int64
db.Model(&User{}).Count(&total)
db.Preload("Articles").Offset(0).Limit(10).Find(&users)
注意:预加载的关联不受分页限制。
db.Preload("Articles", func(db *gorm.DB) *gorm.DB {
return db.Order("created_at desc")
}).First(&user, 1)
关联按创建时间倒序。
博客文章列表
func GetArticles(db *gorm.DB, page, pageSize int) ([]Article, int64, error) {
var articles []Article
var total int64
db.Model(&Article{}).Count(&total)
err := db.Preload("Author").
Preload("Tags").
Preload("Comments", func(db *gorm.DB) *gorm.DB {
return db.Where("status = ?", "approved").Limit(5)
}).
Offset((page - 1) * pageSize).
Limit(pageSize).
Order("created_at desc").
Find(&articles).Error
return articles, total, err
}
用户详情
func GetUserDetail(db *gorm.DB, id uint) (*User, error) {
var user User
err := db.Preload("Profile").
Preload("Articles", func(db *gorm.DB) *gorm.DB {
return db.Select("id, title, created_at").Order("created_at desc").Limit(10)
}).
Preload("Roles.Permissions").
First(&user, id).Error
return &user, err
}
订单详情
func GetOrderDetail(db *gorm.DB, id uint) (*Order, error) {
var order Order
err := db.Preload("Items").
Preload("Items.Product").
Preload("User").
Preload("Payment").
First(&order, id).Error
return &order, err
}
预加载过多
加载太多关联,内存爆炸:
db.Preload("Articles").Preload("Articles.Comments").Find(&users)
用户多时,数据量巨大。解决方案:条件限制、分批加载。
N+1 问题
忘记预加载:
var users []User
db.Find(&users)
for _, u := range users {
db.Model(&u).Association("Roles").Count()
}
应该预加载:
db.Preload("Roles").Find(&users)
预加载条件不生效
db.Where("status = ?", "active").Preload("Articles").Find(&users)
Where 条件作用于用户,不影响预加载的文章。预加载条件要单独写:
db.Where("status = ?", "active").
Preload("Articles", "status = ?", "published").
Find(&users)
预加载是关联查询的标配功能。理解 Preload 和 Joins 的区别,合理使用条件预加载,避免过度预加载。