目录

gorm实现多对多映射以及预加载排序

在Go语言开发中,GORM作为最流行的ORM库之一,为处理复杂的数据库关系提供了强大的支持。

多对多关系是实际业务中最常见的关联模式之一,如用户与角色、文章与标签等。

本文将深入探讨如何使用GORM实现多对多映射,并解决预加载时的排序问题。

一、多对多关系基础概念

多对多关系是指两个实体之间存在双向的一对多关系。例如,一个用户可以有多个角色,一个角色也可以被多个用户拥有。在数据库层面,这种关系需要通过中间表(连接表)来实现

让我们从一个简单的用户-角色模型开始:

package main

import (
    "gorm.io/gorm"
    "time"
)

type User struct {
    ID        uint      `gorm:"primaryKey"`
    Name      string    `gorm:"size:100;not null"`
    Email     string    `gorm:"uniqueIndex;size:150"`
    CreatedAt time.Time
    UpdatedAt time.Time
    // 多对多关联:用户拥有多个角色
    Roles     []Role    `gorm:"many2many:user_roles;"`
}

type Role struct {
    ID          uint      `gorm:"primaryKey"`
    Name        string    `gorm:"uniqueIndex;size:50"`
    Description string    `gorm:"size:200"`
    CreatedAt   time.Time
    UpdatedAt   time.Time
    // 反向关联:角色属于多个用户
    Users       []User    `gorm:"many2many:user_roles;"`
}

在这个基础定义中,gorm:"many2many:user_roles;"标签告诉GORM自动创建名为user_roles的中间表

自定义中间表模型

type UserRole struct {
    UserID    uint      `gorm:"primaryKey"`  // 复合主键的一部分
    RoleID    uint      `gorm:"primaryKey"`  // 复合主键的一部分
    CreatedAt time.Time                     // 关联创建时间
    CreatedBy uint                         // 创建者ID
    ExpiresAt *time.Time                   // 关联过期时间(可选)
}

// 设置自定义中间表
func setupModels(db *gorm.DB) error {
    // 注册自定义中间表
    err := db.SetupJoinTable(&User{}, "Roles", &UserRole{})
    if err != nil {
        return err
    }
    err = db.SetupJoinTable(&Role{}, "Users", &UserRole{})
    if err != nil {
        return err
    }
    
    // 自动迁移所有表
    return db.AutoMigrate(&User{}, &Role{}, &UserRole{})
}

带条件的多对多关系

有时我们需要在关联上添加业务条件,比如只获取有效的用户角色关系:

// 获取用户的有效角色(未过期的)
func GetUserActiveRoles(db *gorm.DB, userID uint) ([]Role, error) {
    var roles []Role
    now := time.Now()
    
    err := db.Joins("JOIN user_roles ON roles.id = user_roles.role_id").
        Where("user_roles.user_id = ? AND (user_roles.expires_at IS NULL OR user_roles.expires_at > ?)", 
              userID, now).
        Order("roles.name ASC").
        Find(&roles).Error
        
    return roles, err
}

二、预加载技术

预加载是 GORM 中解决 N+1 查询问题的关键特性。正确的预加载策略可以显著提升查询性能

(一)基础预加载

// 基础预加载示例
func GetUsersWithRoles(db *gorm.DB) ([]User, error) {
    var users []User
    
    // 预加载Roles关联
    err := db.Preload("Roles").Find(&users).Error
    if err != nil {
        return nil, err
    }
    
    return users, nil
}

(二)条件预加载

对于需要过滤的关联数据,可以使用条件预加载:

// 只预加载特定条件的角色
func GetUsersWithAdminRoles(db *gorm.DB) ([]User, error) {
    var users []User
    
    err := db.Preload("Roles", "name = ?", "admin").Find(&users).Error
    return users, err
}

(三)基础排序实现

// 预加载时对关联数据进行排序
func GetUsersWithSortedRoles(db *gorm.DB) ([]User, error) {
    var users []User
    
    err := db.Preload("Roles", func(db *gorm.DB) *gorm.DB {
        // 在预加载查询中添加排序条件
        return db.Order("roles.name ASC")
    }).Find(&users).Error
    
    return users, err
}

(四)多字段排序

对于复杂的排序需求,可以指定多个排序字段:

func GetUsersWithComplexSortedRoles(db *gorm.DB) ([]User, error) {
    var users []User
    
    err := db.Preload("Roles", func(db *gorm.DB) *gorm.DB {
        return db.Order("roles.priority DESC, roles.name ASC")
    }).Find(&users).Error
    
    return users, err
}

(五)中间表字段排序

当需要根据中间表的字段进行排序时,需要使用JOIN查询:

// 根据中间表字段排序
func GetUsersWithRolesSortedByJoinTime(db *gorm.DB) ([]User, error) {
    var users []User
    
    err := db.Preload("Roles", func(db *gorm.DB) *gorm.DB {
        return db.Joins("JOIN user_roles ON roles.id = user_roles.role_id").
            Order("user_roles.created_at DESC")
    }).Find(&users).Error
    
    return users, err
}

(六)复杂场景的预加载排序

在实际业务中,我们经常会遇到更复杂的排序需求。以下是几个常见场景的解决方案。

多对多嵌套预加载排序

当存在多层关联关系时,需要对每一层都进行排序:

type Permission struct {
    ID   uint   `gorm:"primaryKey"`
    Name string `gorm:"size:100"`
    Code string `gorm:"uniqueIndex;size:50"`
}

// 角色与权限的多对多关系
type RolePermission struct {
    RoleID       uint `gorm:"primaryKey"`
    PermissionID uint `gorm:"primaryKey"`
    GrantedAt    time.Time
}

func SetupNestedModels(db *gorm.DB) error {
    // 添加权限多对多关系
    err := db.SetupJoinTable(&Role{}, "Permissions", &RolePermission{})
    if err != nil {
        return err
    }
    
    return db.AutoMigrate(&Permission{}, &RolePermission{})
}

// 嵌套预加载排序
func GetUsersWithRolesAndPermissions(db *gorm.DB) ([]User, error) {
    var users []User
    
    err := db.Preload("Roles", func(db *gorm.DB) *gorm.DB {
        return db.Order("roles.priority DESC").Preload("Permissions", func(db *gorm.DB) *gorm.DB {
            return db.Order("permissions.name ASC")
        })
    }).Find(&users).Error
    
    return users, err
}

动态排序参数

对于需要根据用户输入动态排序的场景:

type SortParams struct {
    RoleSortField    string
    RoleSortOrder    string
    PermissionSortField string
    PermissionSortOrder string
}

func GetUsersWithDynamicSorting(db *gorm.DB, params SortParams) ([]User, error) {
    var users []User
    
    err := db.Preload("Roles", func(db *gorm.DB) *gorm.DB {
        orderClause := fmt.Sprintf("roles.%s %s", params.RoleSortField, params.RoleSortOrder)
        return db.Order(orderClause).Preload("Permissions", func(db *gorm.DB) *gorm.DB {
            permissionOrder := fmt.Sprintf("permissions.%s %s", 
                params.PermissionSortField, params.PermissionSortOrder)
            return db.Order(permissionOrder)
        })
    }).Find(&users).Error
    
    return users, err
}

注意⚠️:不正确的预加载和排序可能导致严重的性能问题

GORM的多对多关系处理虽然功能强大,但也需要根据具体业务需求进行合理设计和优化。希望本文能为您的Go项目开发提供实用的参考和指导。

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-gorm-preload-sort/

备用原文链接: https://blog.fiveyoboy.com/articles/go-gorm-preload-sort/