Bootstrap

【GORM】Preload与多对多与多对一关系crud

1. 数据库模型设计

多对一关系:Teacher 和 Class
  • 一个 Teacher 可以管理多个 Class。
  • 一个 Class 只能属于一个 Teacher。
type Teacher struct {
    ID    uint   `gorm:"primaryKey"`
    Name  string
    Classes []Class `gorm:"foreignKey:TeacherID"`
}

type Class struct {
    ID        uint   `gorm:"primaryKey"`
    Name      string
    TeacherID uint
}

多对多关系:Student 和 Class
  • 一个 Student 可以选修多个 Class。
  • 一个 Class 可以有多个 Student。
type Student struct {
    ID     uint   `gorm:"primaryKey"`
    Name   string
    Classes []Class `gorm:"many2many:student_classes"`
}

type Class struct {
    ID      uint   `gorm:"primaryKey"`
    Name    string
    Students []Student `gorm:"many2many:student_classes"`
}

注意student_classes 是多对多关系中间表的默认名称。


2. 数据库迁移

使用 AutoMigrate 方法生成表结构:

db.AutoMigrate(&Teacher{}, &Class{}, &Student{})

3. 插入操作

多对一插入
// 创建一个 Teacher 和多个 Class
teacher := Teacher{
    Name: "Mr. Smith",
    Classes: []Class{
        {Name: "Math"},
        {Name: "Science"},
    },
}
db.Create(&teacher)

多对多插入
// 创建 Students 和 Classes,并建立关联
student := Student{Name: "Alice"}
class := Class{Name: "History"}

db.Create(&student)
db.Create(&class)

// 建立关联
db.Model(&student).Association("Classes").Append(&class)

4. 查询操作

多对一查询
// 查询 Teacher 和他管理的 Classes
var teacher Teacher
db.Preload("Classes").First(&teacher, 1) // 按 ID 查询
fmt.Println(teacher)
多对多查询
// 查询某个 Student 及其选修的 Classes
var student Student
db.Preload("Classes").First(&student, 1) // 按 ID 查询
fmt.Println(student)

// 查询某个 Class 及其 Students
var class Class
db.Preload("Students").First(&class, 1) // 按 ID 查询
fmt.Println(class)

5. 更新操作

多对一更新
// 更新 Class 的 Teacher
var class Class
db.First(&class, 1) // 查询 Class
class.TeacherID = 2 // 修改为新的 Teacher ID
db.Save(&class)
多对多更新
// 给 Student 增加一个新的 Class
var student Student
var newClass Class
db.First(&student, 1)
db.First(&newClass, 2)

// 添加关联
db.Model(&student).Association("Classes").Append(&newClass)

6. 删除操作

多对一删除
// 删除一个 Teacher,同时删除其管理的 Classes
var teacher Teacher
db.Preload("Classes").First(&teacher, 1)
db.Delete(&teacher)
多对多删除
// 删除 Student 和 Class 的关联
var student Student
var class Class
db.First(&student, 1)
db.First(&class, 2)

// 删除关联
db.Model(&student).Association("Classes").Delete(&class)

// 删除 Class,本身不会影响 Student
db.Delete(&class)

总结

常见操作表格
操作场景关系类型方法
插入关联数据多对一db.Create(&teacher) (嵌套创建)
插入关联数据多对多db.Model(&student).Association("Classes").Append(&class)
查询关联数据多对一db.Preload("Classes").First(&teacher)
查询关联数据多对多db.Preload("Classes").First(&student)
更新关联数据多对一修改外键字段后调用 db.Save(&class)
更新关联数据多对多使用 db.Model(&student).Association("Classes").Append/Replace/Delete(&class)
删除关联数据多对一删除主记录时可级联删除(需配置外键约束)。
删除关联数据多对多db.Model(&student).Association("Classes").Delete(&class) 解除关联,不删除实际记录。

注意事项

  1. 预加载 (Preload)

    • 查询时建议使用 Preload 明确指定加载关联数据,避免 N+1 查询问题。
  2. 多对多中间表

    • GORM 默认生成中间表,表名为 {模型1}_{模型2},可通过 gorm:"many2many:<表名>" 修改。
  3. 删除关联记录

    • 默认不会删除关联的实际记录,只会解除关联。如果需要级联删除,可以通过外键设置 ON DELETE CASCADE 实现。

GORM 中的 Preload 方法详解

Preload 是 GORM 提供的一个功能,用于在查询主表数据时一并加载其关联表的数据,避免手动多次查询,从而解决 N+1 查询问题


为什么需要 Preload

问题:N+1 查询问题

假设我们有两个模型:TeacherClass,它们之间是多对一关系。一个 Teacher 可以管理多个 Class

type Teacher struct {
    ID      uint   `gorm:"primaryKey"`
    Name    string
    Classes []Class `gorm:"foreignKey:TeacherID"`
}

type Class struct {
    ID        uint   `gorm:"primaryKey"`
    Name      string
    TeacherID uint
}

我们希望查询所有老师,并获取他们所管理的班级。如果直接查询:

var teachers []Teacher
db.Find(&teachers) // 查询所有老师

for i, teacher := range teachers {
    db.Where("teacher_id = ?", teacher.ID).Find(&teachers[i].Classes) // 每个老师再次查询班级
}

问题:

  1. 首次查询所有老师后,还需要多次查询班级表。
  2. 数据量较大时,这种多次查询会大幅增加数据库负担,降低性能。

解决方法:使用 Preload

var teachers []Teacher
db.Preload("Classes").Find(&teachers) // 一次性加载老师及其班级

Preload 的基本用法

1. 简单预加载

使用 Preload 加载指定关联字段的数据。

var teachers []Teacher
db.Preload("Classes").Find(&teachers)

生成的 SQL 查询

SELECT * FROM teachers;
SELECT * FROM classes WHERE teacher_id IN (1, 2, 3, ...);
2. 带条件的预加载

可以为 Preload 方法指定条件,只加载满足条件的关联数据。

var teachers []Teacher
db.Preload("Classes", "name LIKE ?", "%Math%").Find(&teachers)

生成的 SQL 查询

SELECT * FROM teachers;
SELECT * FROM classes WHERE teacher_id IN (1, 2, 3, ...) AND name LIKE '%Math%';

嵌套预加载

如果关联字段本身也有嵌套关联,可以通过链式调用 Preload

假设每个 Class 有多个 Student,我们希望查询老师及其班级和班级中的学生:

type Student struct {
    ID      uint   `gorm:"primaryKey"`
    Name    string
    ClassID uint
}

type Class struct {
    ID        uint   `gorm:"primaryKey"`
    Name      string
    TeacherID uint
    Students  []Student `gorm:"foreignKey:ClassID"`
}

使用嵌套 Preload

var teachers []Teacher
db.Preload("Classes.Students").Find(&teachers)

生成的 SQL 查询

SELECT * FROM teachers;
SELECT * FROM classes WHERE teacher_id IN (1, 2, 3, ...);
SELECT * FROM students WHERE class_id IN (1, 2, 3, ...);

PreloadJoins 的区别

1. Preload 的特点
  • 执行多条 SQL 查询。
  • 自动匹配和填充主表与关联表的数据。
2. Joins 的特点
  • 使用单条 SQL 查询(多表联合查询)。
  • 手动指定如何关联表,返回的是未解析的结果,需要手动映射。

示例:PreloadJoins 的对比

// 使用 Preload
var teachers []Teacher
db.Preload("Classes").Find(&teachers)

// 使用 Joins
var results []struct {
    TeacherName string
    ClassName   string
}
db.Table("teachers").Joins("JOIN classes ON teachers.id = classes.teacher_id").Select("teachers.name AS teacher_name, classes.name AS class_name").Scan(&results)

Preload 的高级功能

1. 多个字段的条件预加载

可以同时为多个关联字段添加 Preload

var teachers []Teacher
db.Preload("Classes").Preload("Classes.Students").Find(&teachers)

2. 仅预加载部分关联字段

如果关联表较大,只需要其中的部分字段,可以使用 Select 限制查询字段:

var teachers []Teacher
db.Preload("Classes", func(db *gorm.DB) *gorm.DB {
    return db.Select("id, name")
}).Find(&teachers)

生成的 SQL 查询

SELECT id, name FROM classes WHERE teacher_id IN (1, 2, 3, ...);

3. 自定义关联键

如果外键不是默认规则,可以使用 gorm:"foreignKey:<外键>;references:<主键>" 指定:

type Teacher struct {
    ID       uint   `gorm:"primaryKey"`
    Name     string
    CustomID string
    Classes  []Class `gorm:"foreignKey:TeacherCustomID;references:CustomID"`
}

type Class struct {
    ID             uint   `gorm:"primaryKey"`
    Name           string
    TeacherCustomID string
}

使用时不需要额外处理,Preload 会自动识别:

var teachers []Teacher
db.Preload("Classes").Find(&teachers)

总结表格

功能方法说明
简单预加载db.Preload("Classes").Find(&teachers)加载主表及其直接关联表。
带条件的预加载db.Preload("Classes", "name LIKE ?", "%Math%").Find(&teachers)为关联表加载添加查询条件。
嵌套预加载db.Preload("Classes.Students").Find(&teachers)加载多级关联数据。
部分字段预加载db.Preload("Classes", func(db *gorm.DB) *gorm.DB { return db.Select("id, name") }).Find()仅加载关联表的部分字段。
自定义外键关联的预加载db.Preload("Classes").Find(&teachers)使用自定义外键时,Preload 会自动识别。

注意事项

  1. 查询性能

    • 如果关联表数据量较大,尽量限制查询条件或返回字段,避免性能问题。
  2. 字段命名冲突

    • 如果主表和关联表中有相同字段名,使用 Select 指定字段避免冲突。
  3. 动态加载

    • 动态控制是否预加载可以通过传递条件实现,比如是否有指定字段条件。
  4. Association 的区别

    • Preload 是在查询时加载关联数据,而 Association 是在数据已加载后进行操作。

https://github.com/0voice

;