背景介绍
gorm 与 mybatis-plus 、hibernate 等 ORM 框架一样,为了应对查询场景居多的现象,支持原生 sql 和 api 两种方式读数据库。
gorm 原生 sql 参见:https://gorm.io/docs/sql_builder.html。
gorm 提供的 api 支持关联插入、关联查询、关联更新、关联删除等功能。这篇文章对各种类型关联查询做以介绍。
关联类型
所谓关联查询就是:在执行查询操作时,如果对象的某个字段也是对象,那就将内部对象也一并查出。以前的做法是先查外围对象,再查内部对象,在内存中组装后返回给前端。
实现关联查询的核心是:依据业务理清楚关联类型以及外键应该放在哪里,在建对象的时候采用标签方式把外键和关联字段格式写正确。
gorm 有默认使用类名+ID
作为默认外键,但是不建议这么使用。推荐使用标签指定外键和关联列,看起来清晰明了,出错少。
belong to (多对一)
链接:https://gorm.io/docs/belongs_to.html
使用场景如:一个员工属于一个公司,但一个公司可以有多个员工。在这种场景下,外键在 User 中,关联到 Company 的主键。
// foreignKey 标签显式指定外键。
// references 标签显式指定外键的关联字段。
type User struct {
gorm.Model
Name string
CompanyID int
Company Company `gorm:"foreignKey:CompanyID;references:ID"`
}
type Company struct {
ID int
Name string
}
对应的表信息如下:
CREATE TABLE `users` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`created_at` datetime(3) DEFAULT NULL,
`updated_at` datetime(3) DEFAULT NULL,
`deleted_at` datetime(3) DEFAULT NULL,
`name` longtext,
`company_id` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_users_deleted_at` (`deleted_at`),
KEY `fk_users_company` (`company_id`),
CONSTRAINT `fk_users_company` FOREIGN KEY (`company_id`) REFERENCES `companies` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
-- 外键 company_id 关联到 companies 的 id 字段。
CREATE TABLE `companies` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` longtext,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
关联查询:
Preloading 就是在外围对象时关联上要查的内部对象。至于怎么关联的,全靠外键。
// 在查 user 对象时将其关联的 compancy 信息一并查出来。
db.Model(&User{}).Preload("Company").Find(&result, "name = ?", user.Name)
data, _ := json.Marshal(result)
println(string(data))
// {"ID":1,"CreatedAt":"2023-12-11T16:57:09.452+08:00","UpdatedAt":"2023-12-11T16:57:09.452+08:00","DeletedAt":null,"Name":"amos","CompanyID":12,"Company":{"ID":12,"Name":"google"}}
完整代码:
package main
import (
"database/sql"
"encoding/json"
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
source := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=Local",
"USERNAME", "PASSWORD", "127.0.0.1", 3306, "test")
sqlDB, _ := sql.Open("mysql", source)
db, _ := gorm.Open(mysql.New(mysql.Config{
Conn: sqlDB,
}), &gorm.Config{})
// 创建表
db.AutoMigrate(&User{}, &Company{})
// 因为已经指定了约束关系,User.companyId 字段会自动被 Company.ID 字段填充。
user := User{
Model: gorm.Model{},
Name: "amos",
Company: Company{
ID: 12,
Name: "google",
},
}
// 关联插入。在插入 user 记录时,也插入 compancy 记录。
db.Create(&user)
var result User
db.Model(&User{}).Preload("Company").Find(&result, "name = ?", user.Name)
data, _ := json.Marshal(result)
println(string(data))
}
type User struct {
gorm.Model
Name string
CompanyID int
Company Company `gorm:"foreignKey:CompanyID;references:ID"`
}
type Company struct {
ID int
Name string
}
has one (一对一)
链接:https://gorm.io/docs/has_one.html
使用场景如:一个用户只能办理一张信用卡。此时外键写法比较随意,外键既可以放在 User
中,也可以放在 CreditCard
中。
// 外键放在 CreditCard 中。
type User struct {
gorm.Model
CreditCard CreditCard `gorm:"foreignKey:UserID;references:ID"`
}
type CreditCard struct {
gorm.Model
Number string
UserID uint
}
// --------------------------------------
// 外键放在 User 中。
type User struct {
gorm.Model
CreditCardId uint
CreditCard CreditCard `gorm:"foreignKey:CreditCardId;references:ID"`
}
type CreditCard struct {
gorm.Model
Number string
}
表:
--- 外键放在 CreditCard 中。
CREATE TABLE `credit_cards` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`created_at` datetime(3) DEFAULT NULL,
`updated_at` datetime(3) DEFAULT NULL,
`deleted_at` datetime(3) DEFAULT NULL,
`number` longtext,
`user_id` bigint unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_credit_cards_deleted_at` (`deleted_at`),
KEY `fk_users_credit_card` (`user_id`),
CONSTRAINT `fk_users_credit_card` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
------------------------------------------------------------------------------------
--- 外键放在 User 中。
CREATE TABLE `users` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`created_at` datetime(3) DEFAULT NULL,
`updated_at` datetime(3) DEFAULT NULL,
`deleted_at` datetime(3) DEFAULT NULL,
`credit_card_id` bigint unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_users_deleted_at` (`deleted_at`),
KEY `fk_users_credit_card` (`credit_card_id`),
CONSTRAINT `fk_users_credit_card` FOREIGN KEY (`credit_card_id`) REFERENCES `credit_cards` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
关联查询:
user := User{
Model: gorm.Model{ID: 1},
CreditCard: CreditCard{
Number: "374738728629",
},
}
db.Create(&user)
var result User
db.Model(&User{}).Preload("CreditCard").Find(&result, "ID = ?", user.ID)
data, _ := json.Marshal(result)
println(string(data))
/*
{
"ID":1,
"CreatedAt":"2023-12-11T17:37:03.935+08:00",
"UpdatedAt":"2023-12-11T17:37:03.935+08:00",
"DeletedAt":null,
"CreditCard":{
"ID":1,
"CreatedAt":"2023-12-11T17:37:03.936+08:00",
"UpdatedAt":"2023-12-11T17:37:03.936+08:00",
"DeletedAt":null,
"Number":"374738728629",
"UserID":1
}
}
*/
has many (一对多)
参考链接:https://gorm.io/docs/has_many.html
使用场景如:一个用户有持有多张信用卡。外键在 CreditCard
中。
type User struct {
gorm.Model
MemberNumber string
CreditCards []CreditCard `gorm:"foreignKey:UserNumber;references:MemberNumber"`
}
type CreditCard struct {
gorm.Model
Number string
UserNumber string
}
关联查询
owner := User{
Model: gorm.Model{},
MemberNumber: "2",
CreditCards: []CreditCard{
{
Model: gorm.Model{},
Number: "1",
},
{
Model: gorm.Model{},
Number: "2",
},
},
}
db.Create(&owner)
var result User
db.Model(&User{}).Preload("CreditCards").Find(&result, "member_number = ?", "2")
data, _ := json.Marshal(result)
println(string(data))
/*
{
"ID":1,
"CreatedAt":"2023-12-11T19:45:48.533+08:00",
"UpdatedAt":"2023-12-11T19:45:48.533+08:00",
"DeletedAt":null,
"MemberNumber":"2",
"CreditCards":[
{
"ID":1,
"CreatedAt":"2023-12-11T19:45:48.534+08:00",
"UpdatedAt":"2023-12-11T19:45:48.534+08:00",
"DeletedAt":null,
"Number":"1",
"UserNumber":"2"
},
{
"ID":2,
"CreatedAt":"2023-12-11T19:45:48.534+08:00",
"UpdatedAt":"2023-12-11T19:45:48.534+08:00",
"DeletedAt":null,
"Number":"2",
"UserNumber":"2"
}
]
}
*/
many to many (多对多)
链接:https://gorm.io/docs/many_to_many.html
使用场景如:一个学生可以选多门课,一门课也可以被多个学生选择。在该场景下,学生和课程的主键必须存在,因为 gorm 还要再生成一张关联表。
// many2many 标签表示多对多关联关系
// Student2Course 是生成的关联表名字。
// Student 和 Course 各自的主键必须存在,最好显式指定。
type Student struct {
SId int `gorm:"primary_key"`
SNo int
Name string
Sex string
Age int
Course []Course `gorm:"many2many:Student2Course"`
}
type Course struct {
CId int `gorm:"primary_key"`
CName string
TeacherName string
Room string
}
表
CREATE TABLE `students` (
`s_id` bigint NOT NULL AUTO_INCREMENT,
`s_no` bigint DEFAULT NULL,
`name` longtext,
`sex` longtext,
`age` bigint DEFAULT NULL,
PRIMARY KEY (`s_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
CREATE TABLE `Courses` (
`c_id` bigint NOT NULL AUTO_INCREMENT,
`c_name` longtext,
`teacher_name` longtext,
`room` longtext,
PRIMARY KEY (`c_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
CREATE TABLE `student2_courses` (
`student_s_id` bigint NOT NULL,
`course_c_id` bigint NOT NULL,
PRIMARY KEY (`student_s_id`,`course_c_id`),
KEY `fk_student2_courses_course` (`course_c_id`),
CONSTRAINT `fk_student2_courses_course` FOREIGN KEY (`course_c_id`) REFERENCES `courses` (`c_id`),
CONSTRAINT `fk_student2_courses_student` FOREIGN KEY (`student_s_id`) REFERENCES `students` (`s_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
-- 约束条件都在生成的关联表里。
关联查询:
stu1 := models.Student{
SNo: 10010,
Name: "amos",
Sex: "male",
Age: 18,
Course: []models.Course{
{
CName: "match",
TeacherName: "Mr.lube",
Room: "No1",
},
{
CName: "Physics",
TeacherName: "Morris.li",
Room: "No2",
},
},
}
stu2 := models.Student{
SNo: 10011,
Name: "sam",
Sex: "male",
Age: 19,
Course: []models.Course{
{
CName: "match",
TeacherName: "Mr.lube",
Room: "No1",
},
{
CName: "Physics",
TeacherName: "Morris.li",
Room: "No2",
},
},
}
db.Create(&stu1)
db.Create(&stu2)
var result []*models.Student
db.Model(&models.Student{}).Preload("Course").Find(&result)
bytes, _ := json.Marshal(result)
println(string(bytes))
/*
[{"SId":1,"SNo":10010,"Name":"amos","Sex":"male","Age":18,"Course":[{"CId":1,"CName":"match","TeacherName":"Mr.lube","Room":"No1"},{"CId":2,"CName":"Physics","TeacherName":"Morris.li","Room":"No2"}]},{"SId":2,"SNo":10011,"Name":"sam","Sex":"male","Age":19,"Course":[{"CId":3,"CName":"match","TeacherName":"Mr.lube","Room":"No1"},{"CId":4,"CName":"Physics","TeacherName":"Morris.li","Room":"No2"}]}]
*/
Polymorphism Association 多态关联
它主要解决一张表怎么与其他多张表关联的问题。
在社交媒体中有如下应用:
- 一个用户可以发表评论。
- 一个图片也可以被评论。
怎么处理评论和用户、博客、图片之间的关系。比如:查用户时怎么关联到他的评论。在比如:如果后期再来个博客的评论,该怎么处理?
/*
1.
2. polymorphicValue 表示多态类型的值,如果不显式指定,默认是类名。
*/
// 评论
type Comment struct {
gorm.Model
Content string
OwnerID uint
OwnerType string
}
// 用户
type person struct {
gorm.Model
Name string
Comments []Comment `gorm:"polymorphic:Owner;polymorphicValue:user"`
}
// 图片
type Image struct {
gorm.Model
~~~~ URL string
Comments []Comment `gorm:"polymorphic:Owner;polymorphicValue:image"`
}
- polymorphic:Owner:polymorphic 是多态关键字,Owner 表示“所有者”,会将“所有者”的 ID 填充到 Comment.OwnerID 字段上。
- polymorphicValue:user:polymorphicValue 表示多态类型,user 表示类型的值,会将 “user” 值填充到 Comment.OwnerType 字段上。如果 polymorphicValue 缺失,则使用类型名填充。
Comments 表
people 表
images 表
关联查询
db.AutoMigrate(&person{}, &Image{}, &Comment{})
user := person{
Name: "John",
Comments: []Comment{
{Content: "Great post!"},
{Content: "terrible post!"},
},
}
image := Image{
Model: gorm.Model{},
URL: "https://example.com/image.jpg",
Comments: []Comment{
{Content: "Nice image!"},
},
}
db.Create(&user)
db.Create(&image)
var fetchedUser person
db.Model(&person{}).Preload("Comments").Find(&fetchedUser, "ID = ?", user.ID)
userBytes, _ := json.Marshal(fetchedUser)
println(string(userBytes))
// {"ID":1,"CreatedAt":"2023-12-11T21:45:08.457+08:00","UpdatedAt":"2023-12-11T21:45:08.457+08:00","DeletedAt":null,"Name":"John","Comments":[{"ID":1,"CreatedAt":"2023-12-11T21:45:08.458+08:00","UpdatedAt":"2023-12-11T21:45:08.458+08:00","DeletedAt":null,"Content":"Great post!","OwnerID":1,"OwnerType":"user"},{"ID":2,"CreatedAt":"2023-12-11T21:45:08.458+08:00","UpdatedAt":"2023-12-11T21:45:08.458+08:00","DeletedAt":null,"Content":"terrible post!","OwnerID":1,"OwnerType":"user"}]}
var fetchedImage Image
db.Model(&Image{}).Preload("Comments").Find(&fetchedImage, "ID = ?", image.ID)
imageBytes, _ := json.Marshal(fetchedImage)
println(string(imageBytes))
// {"ID":1,"CreatedAt":"2023-12-11T21:45:08.459+08:00","UpdatedAt":"2023-12-11T21:45:08.459+08:00","DeletedAt":null,"URL":"https://example.com/image.jpg","Comments":[{"ID":3,"CreatedAt":"2023-12-11T21:45:08.459+08:00","UpdatedAt":"2023-12-11T21:45:08.459+08:00","DeletedAt":null,"Content":"Nice image!","OwnerID":1,"OwnerType":"image"}]}
如果后期来个视频的评论怎么处理?
创建新对象就可以,不用像以前一样新增列。
type Post struct {
gorm.Model
Title string
Content string
Comments []Comment `gorm:"polymorphic:Owner;polymorphicValue:post"`
}
异常现象
官网给的代码有 bug,有时候不能直接使用,可能是版本不一致。
异常 1:
BLOB/TEXT column 'user_number' used in key specification without a key len
解决办法:
type CreditCard struct {
gorm.Model
Number string
UserNumber string `gorm:"size:191"`
}
异常 2:
Failed to add the foreign key constraint. Missing index for constraint 'fk_card_owners_credit_cards' in the referenced table 'card_owners'
解决办法:
关联字段必须有索引。
type CardOwner struct {
gorm.Model
MemberNumber string `gorm:"index"` # 添加索引
CreditCards []CreditCard `gorm:"foreignKey:UserNumber;references:MemberNumber"`
}