Gorm学习(四)基础:关联

本文最后更新于:1 年前

前言

感谢开源项目gin-vue-admin,以及1010工作室的视频教程

一、One To One 一对一

数据库连接例子:

1
2
3
4
5
6
7
8
9
10
11
var db *gorm.DB

func init() {
var err error
//我这里用到数据库是mysql,需要配置DSN属性[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]
dsn := "root:123456@tcp(127.0.0.1:3306)/go_test?charset=utf8&parseTime=True"
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
}

1、Belongs To 属于

belongs to 会与另一个模型建立了一对一的连接。 这种模型的每一个实例都“属于”另一个模型的一个实例。

例如:你有两张表 users 表和 companies 表

  • users –用户表
  • companies –公司表

user 是属于 company 的,就是每个 user 有且只能对应分配给一个 company。

注意:在 User 对象中,有一个和 Company 一样的 CompanyID。 默认情况下, CompanyID 被隐含地用来在 User 和 Company 之间创建一个外键关系, 因此必须包含在 User 结构体中才能填充 Company 内部结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

// `User` 属于 `Company`,`CompanyID` 是外键
type User struct {
gorm.Model
Name string
CompanyID int // 默认情况下, CompanyID 被隐含地用来在 User 和 Company 之间创建一个外键关系
Company Company
}

type Company struct {
ID int
Name string
}

func main() {
//user 里面有 company表的结构 所以只需要自动迁移user表即可
db.AutoMigrate(&User{})
}

1)创建记录

创建在dachang里的用户linzy的记录

1
2
3
4
5
6
7
8
9
10
c := Company{
ID: 1,
Name: "dachang",
}

u := User{
Name: "linzy",
Company: c,
}
db.Create(&u)


不仅 users 表新增记录,companies 表也新增了。这是因为在创建、更新记录时,GORM 会通过 Upsert 自动保存关联及其引用记录。

2)查询记录

1
2
3
var u User
db.Model(&User{}).First(&u)
fmt.Println(u)

注意:我们发现并没有查出我们关联的 companies 表里面的记录,因为我们在使用CRUD的时候需要所关联的结构时,必须要使用预加载 Preload .

1
2
3
var u User
db.Model(&User{}).Preload("Company").First(&u)
fmt.Println(u)

3)重写外键

要定义一个 belongs to 关系,数据库的表中必须存在外键。默认gorm使用(关联属性类型 + 主键)组成外键名,如上面的例子User + ID 组成UserID,UserID就作为Profile的外键。

例如我们想自定义外键,就需要用标签foreignKe来指定外键:

1
2
3
4
5
6
7
8
9
10
11
12
type User struct {
gorm.Model
Name string
CompanyRefer int
Company Company `gorm:"foreignKey:CompanyRefer"`
// 使用 CompanyRefer 作为外键
}

type Company struct {
ID int
Name string
}

4)重写引用

对于 belongs to 关系,GORM 通常使用数据库表,主表(拥有者)的主键值作为外键参考。例如上面的例子,User 中 CompanyRefer 属性作为外键,它和Company 中的ID进行关联,这里 Company 的ID就是关联外键。

我们可以使用标签 references 来更改它,例如:

1
2
3
4
5
6
7
8
9
10
11
12
type User struct {
gorm.Model
Name string
CompanyID string
Company Company `gorm:"references:Code"` // 使用 Code 作为引用
}

type Company struct {
ID int
Code string
Name string
}

5)关联模式

a、查找关联

如果我们想查找指定的 user 匹配的关联记录,可以用Association找users表关联的记录:

1
2
3
4
5
6
7
8
9
10
var user User
db.Where("id = ?", 1).Take(&user)
fmt.Println(user)

var c Company
// `user` 是源模型,它的主键不能为空
// 关系的字段名是 `Company`
// 如果匹配了上面两个要求,会开始关联模式,否则会返回错误
db.Model(&user).Association("Company").Find(&c)
fmt.Println(c)

b、删除关联

user 可以关联 company,同样也可以不关联,但是去库里删很麻烦,用Delete方法删除源模型与参数之间的关系,只会删除引用,不会从数据库中删除这些对象。

删除前:

1
2
3
4
5
var user User
db.Where("id = ?", 1).First(&user)

//一定要指定关联的主键
db.Model(&user).Association("Company").Delete(&Company{ID: 1})

c、添加关联

1
2
3
4
5
var user User
db.Where("id = ?", 1).First(&user)

//一定要指定关联的主键
db.Model(&user).Association("Company").Append(&Company{ID: 1})

d、修改(替换)关联

修改关联就是删除关联后,再添加新的关联。

1
2
3
4
var user User
db.Where("id = ?", 1).First(&user)

db.Model(&user).Association("Company").Replace(&Company{ID: 2})

2、Has One 拥有

has one 与另一个模型建立一对一的关联,但它和一对一关系有些许不同。 这种关联表明一个模型的每个实例都包含或拥有另一个模型的一个实例。

提示:Has one很像属于(belongs to)关系,都是一对一关系,区别是Has One关系和Belongs To关系,持有关联Model属性的关系是相反的,例如:A 关联 B,Has One关系通常是A 结构体持有B属性, belongs to关系则是B结构体持有A。

例如:你有两张表 users 表和 credit_cards 表

  • users –用户表
  • credit_cards –信用卡表

user 是拥有 creditcard 的,creditcard 有且只能被一个 user 拥有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// User 有一张 CreditCard,UserID 是外键
type User struct {
gorm.Model
CreditCard CreditCard
}

type CreditCard struct {
gorm.Model
Number string
UserID uint
}

func main() {
//这里需要先创建users表,再创建credit_cards表
db.AutoMigrate(&User{}, &CreditCard{})
}

1)创建记录

创建用户拥有信用卡号为123456的记录

1
2
3
4
5
6
7
8
c := CreditCard{
Number: "123456",
}

u := User{
CreditCard: c,
}
db.Create(&u)


2)查询记录

1
2
3
var u User
db.Model(&User{}).Preload("CreditCard").First(&u)
fmt.Println(u)

3)重写外键

对于 has one 关系,同样必须存在外键字段。拥有者将把属于它的模型的主键保存到这个字段。

默认情况下Has One关系的外键由持有关联属性的类型名 + 主键 组成外键名,如上例,User关联CreditCard的外键就是User + ID = UserID。

如果你想要使用另一个字段来保存该关系,你同样可以使用标签 foreignKey 来更改它,例如:

1
2
3
4
5
6
7
8
9
10
11
type User struct {
gorm.Model
CreditCard CreditCard `gorm:"foreignKey:UserName"`
// use UserName as foreign key
}

type CreditCard struct {
gorm.Model
Number string
UserName string
}

4)重写引用

默认情况下,保存User的时候,会自动将User的主键保存到外键UserID中,关联查询的时候,也会使用外键和关联外键进行关联进行查询,这里User的ID就是关联外键

可以使用标签 references 来更改它,例如:

1
2
3
4
5
6
7
8
9
10
11
type User struct {
gorm.Model
Name string `gorm:"index"`
CreditCard CreditCard `gorm:"foreignkey:UserName;references:name"`
}

type CreditCard struct {
gorm.Model
Number string
UserName string
}

5)关联模式

a、查找关联

如果我们想查找指定的 user 匹配的关联记录,可以用Association找users表关联的记录:

1
2
3
4
5
6
7
8
9
10
var user User
db.Where("id = ?", 1).Take(&user)
fmt.Println(user)

var c CreditCard
// `user` 是源模型,它的主键不能为空
// 关系的字段名是 `CreditCard`
// 如果匹配了上面两个要求,会开始关联模式,否则会返回错误
db.Model(&user).Association("CreditCard").Find(&c)
fmt.Println(c)

b、删除关联

user 可以关联 CreditCard,同样也可以不关联,但是去库里删很麻烦,用Delete方法删除源模型与参数之间的关系,只会删除引用,不会从数据库中删除这些对象。

删除前:

1
2
3
4
5
6
7
8
9
user := User{}
db.Where("id = ?", "1").First(&user)

//一定要指定关联的主键
db.Model(&user).Association("CreditCard").Delete(&CreditCard{
Model: gorm.Model{
ID: 1,
},
})

c、添加关联

1
2
3
4
5
6
7
8
9
var user User
db.Where("id = ?", 1).First(&user)

//一定要指定关联的主键
db.Model(&user).Association("CreditCard").Append(&CreditCard{
Model: gorm.Model{
ID: 1,
},
})

d、修改(替换)关联

修改关联就是删除关联后,再添加新的关联。

1
2
3
4
5
6
7
8
user := User{}
db.Where("id = ?", "2").First(&user)

db.Model(&user).Association("CreditCard").Replace(&CreditCard{
Model: gorm.Model{
ID: 1,
},
})

二、Has Many 一对多

has many 与另一个模型建立了一对多的连接。 不同于 has one,拥有者可以有零或多个关联模型。

例如:你有两张表 users 表和 credit_cards 表

  • users –用户表
  • credit_cards –信用卡表

user 是拥有多张 creditcard的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// User 有多张 CreditCard,UserID 是外键
type User struct {
gorm.Model
CreditCards []CreditCard
}

type CreditCard struct {
gorm.Model
Number string
UserID uint
}

func main() {
db.AutoMigrate(&User{}, &CreditCard{})
}

1、创建记录

创建用户拥有两种信用卡的记录

1
2
3
4
5
6
7
8
9
10
c1 := CreditCard{
Number: "123456",
}
c2 := CreditCard{
Number: "8791265",
}
u := User{
CreditCards: []CreditCard{c1, c2},
}
db.Create(&u)


2、查询记录

1
2
3
var user User
db.Model(&User{}).Preload("CreditCards").Find(&user)
fmt.Println(user)

3、预加载

GORM 允许在 Preload 的其它 SQL 中直接加载关系

1)预加载全部

与创建、更新时使用 Select 类似,clause.Associations 也可以和 Preload一起使用,它可以用来 预加载全部关联,例如:

1
2
3
4
5
6
7
8
9
10
11
type User struct {
gorm.Model
Name string
CompanyID uint
Company Company
Role Role
Orders []Order
}

db.Preload(clause.Associations).Find(&users)

2)嵌套预加载

clause.Associations 不会预加载嵌套的关联,如果你在需要用嵌套的关联,你可以使用嵌套预加载 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// User 有多张 CreditCard,UserID 是外键
type User struct {
gorm.Model
CreditCards []CreditCard
}

type CreditCard struct {
gorm.Model
Number string
UserID uint
Info Info
}

type Info struct {
ID uint
Name string
Age int
CreditCardID int
}



使用之前预加载查询记录查询:

1
2
3
var user User
db.Model(&User{}).Preload("CreditCards").Find(&user)
fmt.Println(user)

我们只能查到主表对应关联的表结构记录,所以我们这里需要用到嵌套预加载的方式拿到我们需要的数据。

1
2
3
4
var user User
//CreditCards.Info关联的下层结构
db.Model(&User{}).Preload("CreditCards.Info").Preload("CreditCards").Find(&user)
fmt.Println(user)

或者 使用 自定义预加载 SQL

1
2
3
4
5
var user User
db.Model(&User{}).Preload("CreditCards", func(db *gorm.DB) *gorm.DB {
return db.Preload("Info")
}).Find(&user)
fmt.Println(user)

3)带条件的预加载

有时候我们需要查询特定的关联结构记录时,可以使用带条件的 Preload 关联,类似于内联条件。

1
2
3
4
var user User
//找到信用卡号不等于`123456`的记录
db.Model(&User{}).Preload("CreditCards.Info").Preload("CreditCards", "Number <> ?", "123456").Find(&user)
fmt.Println(user)

a、Joins 预加载

可以直接查询关联的下层关联结构,但是这时候不能直接使用带条件的 Preload 关联了,例如我要找信用卡用户不是”linzy“的时候:

1
2
3
var user User
db.Model(&User{}).Preload("CreditCards.Info", "name <> ?", "linzy").Preload("CreditCards").Find(&user)
fmt.Println(user)

他虽然满足了条件,但不是我们想要的结果,这个时候需要用到自定义加载SQL以及 Joins 预加载

1
2
3
4
5
var user User
db.Model(&User{}).Preload("CreditCards", func(db *gorm.DB) *gorm.DB {
return db.Joins("Info").Where("name <> ?", "linzy")
}).Find(&user)
fmt.Println(user)

注意:Preload 在一个单独查询中加载关联数据。而 Join Preload 会使用 left join 加载关联数据。
Join Preload 适用于一对一的关系,例如: has one, belongs to。

4、多态关联

GORM 为 has onehas many 提供了多态关联支持,它会将拥有者实体的表名、主键都保存到多态类型的字段中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Dog struct {
ID int
Name string
Toys []Toy `gorm:"polymorphic:Owner;"`
}

type Toy struct {
ID int
Name string
OwnerID int
OwnerType string
}

func main() {
db.AutoMigrate(&Dog{}, &Toy{})
db.Create(&Dog{Name: "dog1", Toys: []Toy{{Name: "toy1"}, {Name: "toy2"}}})
// INSERT INTO `dogs` (`name`) VALUES ("dog1")
// INSERT INTO `toys` (`name`,`owner_id`,`owner_type`) VALUES ("toy1","1","dogs"), ("toy2","1","dogs")
}


5、关联模式

关联模式下不同关系的CRUD关联都是类似的写法,就不多阐述了。

1)清空关联

删除源模型与关联之间的所有引用,但不会删除这些关联。

1
2
3
var user User
db.First(&user)
db.Model(&user).Association("CreditCards").Clear()

2)关联计数

返回当前关联的总数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var user User
db.First(&user)
//添加关联
db.Model(&user).Association("CreditCards").Append(&CreditCard{
Model: gorm.Model{
ID: 1,
},
}, &CreditCard{
Model: gorm.Model{
ID: 2,
},
})

//关联计数
count := db.Model(&user).Association("CreditCards").Count()
fmt.Println(count)

三、Many To Many 多对多

Many to Many 会在两个 model 中添加一张连接表
例如,你有两张表 users 表和 languages 表

  • users –用户表
  • languages –语言表

一个 用户可以说多种 语言,多个 用户也可以说一种 语言。

1
2
3
4
5
6
7
8
9
10
// User 拥有并属于多种 language,`user_languages` 是连接表
type User struct {
gorm.Model
Languages []Language `gorm:"many2many:user_languages;"`
}

type Language struct {
gorm.Model
Name string
}

当使用 GORM 的 AutoMigrate 为 User 创建表时,GORM 会自动创建连接表。
某种意义上这种其实还是一对多的关系,反向引用的形式才是真正多对多的关系。

1、反向引用

我既可以用 User 创建多条关联,也可以通过 Language 多条关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// User 拥有并属于多种 language,`user_languages` 是连接表
type User struct {
gorm.Model
Languages []Language `gorm:"many2many:user_languages;"`
}

type Language struct {
gorm.Model
Name string
Users []User `gorm:"many2many:user_languages;"`
}

func main() {
db.AutoMigrate(&User{}, &Language{})
}

2、创建记录

  • 创建用户拥有两种语言的记录
1
2
3
4
5
6
7
8
9
10
l1 := Language{
Name: "中文",
}
l2 := Language{
Name: "英文",
}
u1 := User{
Languages: []Language{l1, l2},
}
db.Create(&u1)

  • 创建外星语被两个用户所使用的记录:
1
2
3
4
5
6
7
u := User{}
l := Language{
Name: "外星语",
//也可以直接指定创建好的记录的主键
Users: []User{u, User{Model: gorm.Model{ID: 1}}},
}
db.Create(&l)



3、查找记录

1
2
3
4
u := User{}
db.Where("id = ?", 1).Find(&u)
db.Model(&User{}).Preload("Languages").Find(&u)
fmt.Println(u)

4、关联模式

1)查询关联

查询user 对应的关联记录

1
2
3
4
5
u := User{}
db.Where("id = ?", 2).Find(&u)
var l []Language
db.Model(&u).Association("Languages").Find(&l)
fmt.Println(l)

2)添加关联

1
2
3
4
5
6
7
8
9
u := User{}
db.Where("id = ?", 2).Find(&u)
l1 := Language{
Name: "俄语",
}
l2 := Language{
Name: "法语",
}
db.Model(&u).Association("Languages").Append(&l1, &l2)

注意:添加关联的同时,不仅连接表会添加新关联记录,关联的结构表也会添加新的记录,除非指定的是表里已存在数据则不会添加。

3)删除关联

删除关联并不会删除关联的结构表里面的数据。

1
2
3
4
5
6
7
u := User{}
db.Where("id = ?", 2).Find(&u)
db.Model(&u).Association("Languages").Delete(&Language{
Model: gorm.Model{
ID: 3,
},
})

4)修改(替换)关联

1
2
3
4
5
6
7
u := User{}
db.Where("id = ?", 2).Find(&u)
db.Model(&u).Association("Languages").Replace(&Language{
Model: gorm.Model{
ID: 3,
},
})

修改前:

修改后:

注意:在多对多使用修改关联会把连接表里面所有关于主表主键与外键关联的记录全部替换掉,慎用此操作。

5)清空关联

1
2
3
u := User{}
db.Where("id = ?", 2).Find(&u)
db.Model(&u).Association("Languages").Clear()

四、小结

一对一、一对多和多对多关系下的CRUD关联操作,在应对复杂的数据表关系时也能更好的应对,本着以实战操作为学习方法,最好结合官方文档上手是最好最正确的。

关联的类型 描述
一对一 两个表在关联的每一侧只能具有一个记录。

每个主键值或者与相关表中的所有记录都无关,或者仅与一个记录相关。

大多数一对一关联是由业务规则强制的,而不是从数据自然流动。 若没有这样的规则,通常可以将两个表相结合,而不会违反任何规范化规则。
一对多 主键表只包含一个记录,其与相关表中零个、一个或多个记录相关。
多对多 两个表中的每个记录都可以与另一个表中的零个或任意数目个记录相关。 由于关系系统不能直接适应关联,因此这些关联需要第三个表,其称为关联或链接表。

关联标签以及自定义SQL预加载内容将留到事务中。
若有写的错误的或者需要改进的地方,希望能直接指出,再次感谢GVA淼哥的教程!


Gorm学习(四)基础:关联
https://gopherlinzy.github.io/2022/10/11/gorm-associate/
作者
孙禄毅
发布于
2022年10月11日
许可协议