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) 解除关联,不删除实际记录。 |
注意事项
-
预加载 (
Preload
):- 查询时建议使用
Preload
明确指定加载关联数据,避免 N+1 查询问题。
- 查询时建议使用
-
多对多中间表:
- GORM 默认生成中间表,表名为
{模型1}_{模型2}
,可通过gorm:"many2many:<表名>"
修改。
- GORM 默认生成中间表,表名为
-
删除关联记录:
- 默认不会删除关联的实际记录,只会解除关联。如果需要级联删除,可以通过外键设置
ON DELETE CASCADE
实现。
- 默认不会删除关联的实际记录,只会解除关联。如果需要级联删除,可以通过外键设置
GORM 中的 Preload
方法详解
Preload
是 GORM 提供的一个功能,用于在查询主表数据时一并加载其关联表的数据,避免手动多次查询,从而解决 N+1 查询问题。
为什么需要 Preload
?
问题:N+1 查询问题
假设我们有两个模型:Teacher
和 Class
,它们之间是多对一关系。一个 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) // 每个老师再次查询班级
}
问题:
- 首次查询所有老师后,还需要多次查询班级表。
- 数据量较大时,这种多次查询会大幅增加数据库负担,降低性能。
解决方法:使用 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, ...);
Preload
和 Joins
的区别
1. Preload
的特点
- 执行多条 SQL 查询。
- 自动匹配和填充主表与关联表的数据。
2. Joins
的特点
- 使用单条 SQL 查询(多表联合查询)。
- 手动指定如何关联表,返回的是未解析的结果,需要手动映射。
示例:Preload
和 Joins
的对比
// 使用 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 会自动识别。 |
注意事项
-
查询性能:
- 如果关联表数据量较大,尽量限制查询条件或返回字段,避免性能问题。
-
字段命名冲突:
- 如果主表和关联表中有相同字段名,使用
Select
指定字段避免冲突。
- 如果主表和关联表中有相同字段名,使用
-
动态加载:
- 动态控制是否预加载可以通过传递条件实现,比如是否有指定字段条件。
-
与
Association
的区别:Preload
是在查询时加载关联数据,而Association
是在数据已加载后进行操作。