关联预加载

关联预加载是优化关联查询的关键。理解不同预加载策略的特点,选择合适的方式,能显著提升性能。

为什么需要预加载

不预加载时,关联字段是空的:

var user User
db.First(&user, 1)
fmt.Println(user.Roles)

Roles 是空切片,因为 GORM 不会自动加载关联。

Preload 基本用法

var user User
db.Preload("Roles").First(&user, 1)
fmt.Println(user.Roles)

执行两条 SQL:

  1. 查用户
  2. 查用户的角色

嵌套预加载

加载多层关联:

type User struct {
    ID    uint
    Roles []Role
}

type Role struct {
    ID          uint
    Permissions []Permission
}

db.Preload("Roles.Permissions").First(&user, 1)

执行三条 SQL:

  1. 查用户
  2. 查角色
  3. 查权限

多关联预加载

同时加载多个关联:

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 篇已发布文章。

Joins 预加载

用 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 的区别,合理使用条件预加载,避免过度预加载。