查询钩子

查询操作只有一个钩子:AfterFind。在查询完成后执行,适合处理数据转换和初始化。

AfterFind

查询后执行:

func (u *User) AfterFind(tx *gorm.DB) error {
    u.Name = strings.TrimSpace(u.Name)
    return nil
}

执行时机

AfterFind 在以下操作后触发:

db.First(&user)
db.Find(&users)
db.Take(&user)
db.Last(&user)

数据转换

查询后转换数据格式:

type User struct {
    ID        uint
    AvatarURL string
}

func (u *User) AfterFind(tx *gorm.DB) error {
    if u.AvatarURL != "" && !strings.HasPrefix(u.AvatarURL, "http") {
        u.AvatarURL = "https://cdn.example.com/" + u.AvatarURL
    }
    return nil
}

计算字段

查询后计算派生字段:

type Order struct {
    ID          uint
    TotalAmount float64
    PaidAmount  float64
    RemainAmount float64 `gorm:"-"`
}

func (o *Order) AfterFind(tx *gorm.DB) error {
    o.RemainAmount = o.TotalAmount - o.PaidAmount
    return nil
}

gorm:"-" 标签表示不映射到数据库字段。

关联数据加载

查询后加载额外数据:

func (u *User) AfterFind(tx *gorm.DB) error {
    var count int64
    tx.Model(&Order{}).Where("user_id = ?", u.ID).Count(&count)
    u.OrderCount = count
    return nil
}

注意:这会产生 N+1 查询问题。更好的方式是用预加载或在查询时计算。

JSON 解析

解析存储的 JSON 数据:

type User struct {
    ID      uint
    Config  string
    Settings map[string]interface{} `gorm:"-"`
}

func (u *User) AfterFind(tx *gorm.DB) error {
    if u.Config != "" {
        json.Unmarshal([]byte(u.Config), &u.Settings)
    }
    return nil
}

时间格式化

格式化时间显示:

type Article struct {
    ID          uint
    PublishedAt time.Time
    PublishedAtStr string `gorm:"-"`
}

func (a *Article) AfterFind(tx *gorm.DB) error {
    if !a.PublishedAt.IsZero() {
        a.PublishedAtStr = a.PublishedAt.Format("2006-01-02 15:04:05")
    }
    return nil
}

状态计算

根据多个字段计算状态:

type Order struct {
    ID          uint
    Status      string
    PaidAmount  float64
    TotalAmount float64
    DisplayStatus string `gorm:"-"`
}

func (o *Order) AfterFind(tx *gorm.DB) error {
    switch {
    case o.Status == "cancelled":
        o.DisplayStatus = "已取消"
    case o.PaidAmount >= o.TotalAmount:
        o.DisplayStatus = "已支付"
    case o.PaidAmount > 0:
        o.DisplayStatus = "部分支付"
    default:
        o.DisplayStatus = "待支付"
    }
    return nil
}

批量查询

批量查询时,每条记录都会触发 AfterFind:

var users []User
db.Find(&users)

每个 user 都会执行 AfterFind。

预加载关联

预加载的关联也会触发 AfterFind:

type User struct {
    ID      uint
    Profile Profile
}

func (p *Profile) AfterFind(tx *gorm.DB) error {
    p.Bio = strings.TrimSpace(p.Bio)
    return nil
}

db.Preload("Profile").Find(&users)

Profile 的 AfterFind 也会执行。

性能考虑

AfterFind 在每条记录上执行,可能影响性能:

func (u *User) AfterFind(tx *gorm.DB) error {
    // 避免 N+1 查询
    return nil
}

如果需要在 AfterFind 中查询数据库,考虑用批量查询或预加载。

跳过钩子

db.Session(&gorm.Session{SkipHooks: true}).Find(&users)

实际案例

商品价格计算

type Product struct {
    ID          uint
    Price       float64
    Discount    float64
    FinalPrice  float64 `gorm:"-"`
}

func (p *Product) AfterFind(tx *gorm.DB) error {
    p.FinalPrice = p.Price * (1 - p.Discount/100)
    return nil
}

用户权限加载

type User struct {
    ID          uint
    RoleID      uint
    Permissions []string `gorm:"-"`
}

func (u *User) AfterFind(tx *gorm.DB) error {
    var role Role
    if err := tx.Preload("Permissions").First(&role, u.RoleID).Error; err != nil {
        return err
    }
    
    for _, p := range role.Permissions {
        u.Permissions = append(u.Permissions, p.Code)
    }
    return nil
}

文章内容处理

type Article struct {
    ID      uint
    Content string
    Summary string `gorm:"-"`
}

func (a *Article) AfterFind(tx *gorm.DB) error {
    if len(a.Content) > 200 {
        a.Summary = a.Content[:200] + "..."
    } else {
        a.Summary = a.Content
    }
    return nil
}

小结

AfterFind 是唯一的查询钩子,适合数据转换、计算字段、格式化等场景。注意性能影响,避免在钩子中执行额外查询。