​ 之前写了一篇关于关联的博客,当时是跟着B站的视频学的,但是学完后感觉对关联的理解,特别是表与表之间的关系,CRUD的操作还不是特别数量,期间写项目的时候总怪怪的,因此回来重新学习一下,这次完全就是跟着GORM文档学习,本篇博客不会描述具体怎么写代码,就记录一下我对这些关联的认识

Belongs To | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

开始之前

先铺垫一下外键,某个结构体A中有xxxID,则xxxID为外键,同时在图表中表示A指向xxx

A指向xxx,所以xxx不能随意修改删除

Belongs To#


  • 属于#

记住**”属于”**这两个字,对理解这个关联挺有帮助的

个人感觉其实Belongs To并不能单纯地被认为是一对一关系,他的一对一关系指的是某个用户属于某个公司,但是从某个角度来看,其实也有很多用户属于这个公司,因此公司与每个员工来说是一对一的,结合起来确实一对多的

1
2
3
4
5
6
7
8
9
10
11
type User struct {
gorm.Model
Name string
CompanyID int
Company Company
}

type Company struct {
ID int
Name string
}

image-20240508195545023

由此可见虽然是User属于company,但是却是user指向company,跟我们传统的思想不一样,所以这个是反向思维的,不如直接按照刚才外键方法理解

User结构体是包含公司结构体的,使用 BelongsTo 可以让你在查询用户时自动填充所属的公司信息(需要使用Preload),同时也表示我们在创建User的时候需要填充所属公司信息

不如带入到个人与公司这个环境中,你辞职了公式不会倒闭,但是公司倒闭了你肯定已经辞职了,按照这个逻辑就可以清楚解释为什么我们可以删除user而对应的公司不删除,我们删除公司的时候总是需要先将有关联的员工删除掉再注销公司

CRUD

实体关联 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

具体写法我就不赘述了

  • 增加

    创建User的时候需要填充所属公司信息

  • 删除

    删除User并不会直接把Company删除,毕竟只是一个员工辞职而已

  • 更新

    个人觉得正常人应该不会去修改关联,因此平常更新也就是平常的数据库操作更新,也就是与关联无关的字段的更新(上面的链接中也有介绍怎么修改关联)

  • 查找

    使用preload,记住要沿着箭头去查询,要不然就会变成

    1
    2
    User: unsupported relations for schema Company
    [1.771ms] [rows:1] SELECT * FROM `company` WHERE id = 5 ORDER BY `company`.`id` LIMIT 1

注意:

外键这个角度出发,我们可以发现其实只要别碰关联的外键,其实两个表就是分离的

但是总是会有抽象,比如说我

当已经存在一个image-20240508202250988这个公司的时候

如果你还是这么写

image-20240508202410138

你会发现image-20240508202442774(这是公司的图片)image-20240508202457008(这是员工的图片)

GORM为我们重新创建了一个公司,是不是很神奇抽象,但是其实我们从逻辑层面(属于)来讲的话,公司怎么可能随随便便就改名字呢,我们不可能在创建员工的同时把公司给改了,要不然更加抽象

下面不如总结一下创建的时候的几种情况(最主要是通过外键关系来添加,其他字段都是不重要的)

  • func try() error {
        u := User{
            Name:      "xxxx",
            Company: Company{
                Name: "xxxx",
            },
        }
        return _db.Create(&u).Error
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    这种情况下,如果xxxx公司不存在的时候,Gorm会自动创建一个xxxx公司给这个员工,同时员工包含着这个公司的某些信息,如果公司已经成立了,他会重新创建一个同名的公司,而不是将员工属于原公司

    * ````go
    func try() error {
    u := User{
    Name: "xxxx",
    CompanyID: 6,
    }
    return _db.Create(&u).Error
    }
    我觉得这个才是最常见的写法,因为当我们加入某个公司的时候,肯定只要把公司的名号给个人就行了,如果你想要查公司的信息,就使用preload,外键专属
  • func try() error {
        u := User{
           Name:      "xxxx",
           CompanyID: 6,
           Company: Company{
              Name: "xxxx",
           },
        }
        return _db.Create(&u).Error
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82

    这种情况就很抽象了,上面讲了,很无语。

    ### 总结

    * **属于**
    * **外键**

    真正有难度的只有涉及到外键的情况下,结合属于这个逻辑情况,去考虑

    # Has Many

    ----

    * ### **拥有**

    我愿称之为 **纯粹的一对一**

    > 例如,您的应用包含 user 和 credit card 模型,且每个 user 只能有一张 credit card。

    ````go
    type Student struct {
    ID int
    Name string
    IDCard IDCard // 记得不要写成 IDCard,不然他会自引用
    }

    type IDCard struct {
    ID int
    StudentID int
    Number string
    }
    ````



    ![image-20240508210009309](https://echin-h.oss-cn-hangzhou.aliyuncs.com/img/image-20240508210009309.png)

    Gorm中的信用卡的例子我觉得不太好,因为个人觉得一个人也可以有很多信用卡,很多信用卡属于一个人说的过去,因此不如写成某个学生的学生证

    按照外键的惯例,是学生证结构体指向学生结构体,按照拥有这个逻辑,学生拥有学生证

    其实我突然发现,所谓的has one其实也是从某种层面来讲的

    **CRUD**

    [实体关联 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.](https://gorm.io/zh_CN/docs/associations.html#Association-Mode)

    * **创建**

    下面我有好几个尝试的案例可以先看看(原本的表中没有数据)

    * **第一个**

    最简单的创建,跟belongs to 一样

    ````go
    func try() error {
    return _db.Model(&Student{}).Create(&Student{
    Name: "jinzhu",
    IDCard: IDCard{
    Number: "411422198912111122",
    },
    }).Error
    }
    ````

    ![image-20240508211233514](https://echin-h.oss-cn-hangzhou.aliyuncs.com/img/image-20240508211233514.png)(Student)

    ![image-20240508211308610](https://echin-h.oss-cn-hangzhou.aliyuncs.com/img/image-20240508211308610.png)(IDCard)



    * **第二个**

    ```go
    func try() error {
    return _db.Model(&IDCard{}).Create(&IDCard{
    Number: "123456",
    StudentID: 1,
    }).Error
    }
    ![image-20240508211457581](C:/Users/%E4%BD%95%E4%B8%80%E5%B7%9D/AppData/Roaming/Typora/typora-user-images/image-20240508211457581.png)(IDCard) 有没有发现什么,我现在是创建IDCard,这也是能够创建的其实 **但是有一个问题!!!!!** 现在一共有两条记录 ![image-20240508212151404](https://echin-h.oss-cn-hangzhou.aliyuncs.com/img/image-20240508212151404.png) 但我使用这个函数进行查找的时候
    1
    2
    3
    4
    5
    6
    func try() error {
    stu := make([]Student, 0)
    err := _db.Preload("IDCard").First(&stu).Error
    fmt.Println(stu)
    return err
    }
    输出为
    1
    2
    3
    4
    === RUN   TestGormInit
    [{1 jinzhu {2 1 123456}}]
    --- PASS: TestGormInit (0.04s)
    PASS
    但我删除第二条记录的时候 输出为
    1
    2
    3
    4
    === RUN   TestGormInit
    [{1 jinzhu {1 1 411422198912111122}}]
    --- PASS: TestGormInit (0.05s)
    PASS
    说明虽然我能够生成很多学生证,但是查找的时候永远是按照最新一张学生证来查找的,哪怕你使用find()都没有用 从这个角度来看还是一对一的 * **第三个** 虽然不存在ID=2的学生,但是我直接创建ID=2的学生的学生证
    1
    2
    3
    4
    5
    6
    func try() error {
    return _db.Create(&IDCard{
    StudentID: 2,
    Number: "123456",
    }).Error
    }
    从拥有的逻辑来看,结果毋庸置疑
    1
    2
    Error 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`pick`.`id_card`, CONSTRAINT `fk_student_id_card` FOREIGN KEY (`student_id`) REFERENCES `student` (`id`))
    [9.348ms] [rows:0] INSERT INTO `id_card` (`student_id`,`number`) VALUES (2,'123456')
    ### 总结 写到这里其实我已经凌乱了,怎么会这么多情况(还只是我个人想出来的情况),那我以后怎么写,有必要总结一下 1. 正常情况通过Stu来生成IDCard 2. 正常情况下一对一查询IDCard 3. 不要乱七八糟去生成IDCard,太丑陋了
  • 更新

    同上,个人觉得正常人应该不会去修改关联,因此平常更新也就是平常的数据库操作更新,也就是与关联无关的字段的更新(上面的链接中也有介绍怎么修改关联)

  • 删除

    同Belongs To

  • 查询

    同Belongs To尼玛个der

    查询的时候要按**”拥有”**这个逻辑来查询,学生拥有学生证,因此应该是通过学生去查学生证

    其实本来我还想看看Belongs To中,能不能一个员工有多个公司,但是后来我发现我好抽象

    正确

    1
    2
    3
    4
    5
    6
    7
    func try() error {
    var stu Student
    err := _db.Preload("IDCard").First(&stu, 1).Error
    fmt.Println(stu)
    return err
    //{1 jinzhu {1 1 411422198912111122}}
    }

    错误

    1
    2
    3
    4
    5
    6
    7
    8
    9
    func try() error{
    var card IDCard
    err := _db.Preload("Student").First(&card, 1).Error
    fmt.Println(card)
    return err
    // 2024/05/08 21:30:20 H:/today/DB/mysql.go:78 Student: unsupported relations for schema IDCard
    // [3.565ms] [rows:1] SELECT * FROM `id_card` WHERE `id_card`.`id` = 1 ORDER BY `id_card`.`id` LIMIT 1
    // {1 1 411422198912111122}
    }

总结#

其实 Belongs To 与 Has Many到底有什么区别,这是我之前一直关注的

但是现在我发现,真正的CRUD都是相同的(使用关联的知识)

因此我们更多的时候应该通过逻辑意义上去CRUD,而不是像我上面这样抽象,在符合逻辑下的处理后,再使用关联中的知识进行CRUD还是没啥问题的,没啥大病反正

根据官方文档去使用外键!!!!

最后贴一个老板的写法,去除外键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Teacher struct {
ID int
Name string
Password string
Father Father `gorm:"-"`
}

type Father struct {
ID int
Name string
}

func try() error {
var teach Teacher
_db.Model(&teach).Where("id = ?", 1).First(&teach)
fmt.Println(teach)
_db.Where("id = ?", 1).First(&teach.Father)
fmt.Println(teach)
return nil
}

Has Many#


  • 一对多

直接上案例吧,这个挺简单的,跟has one有点像

基本写法:

1
2
3
4
5
6
7
8
9
10
11
type Dog struct {
gorm.Model
Name string
GirlGodID uint
}

type GirlGod struct {
gorm.Model
Name string
Dogs []Dog
}

和 has one 一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
d := Dog{
Model: gorm.Model{
ID: 1,
},
Name: "汪汪1号",
}
d1 := Dog{
Model: gorm.Model{
ID: 2,
},
Name: "汪汪2号",
}

g := GirlGod{
Model: gorm.Model{
ID: 1,
},
Name: "奇奇",
Dogs: []Dog{d, d1},
}

查询

  1. 普通查找

    1
    2
    var girl GirlGod
    _db.Preload("Dogs").First(&girl) // 记住是Dogs,不是Dog,是这个字段
  2. 查找数组中的指定元素(简单)

    1
    2
    var girl GirlGod
    _db.Preload("Dogs", "name = ?", "汪汪1号").First(&girl) // 记住是Dogs,不是Dog,是这个字段
  3. 查找数组中的指定元素(复杂形式)

    1
    2
    3
    4
    5
    var girl GirlGod
    _db.Preload("Dogs", func(db *gorm.DB) *gorm.DB {
    return db.Where("name = ?", "汪汪1号")
    }).First(&girl)
    fmt.Println(girl)

链式查询#

现在复杂一下结构体

一个 has one 一个 has many

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Info struct {
gorm.Model
Money int
DogID uint
}

type Dog struct {
gorm.Model
Name string
GirlGodID uint
Info Info
}

type GirlGod struct {
gorm.Model
Name string
Dogs []Dog
}

然后按照刚才的老套路查询一下

1
2
3
4
5
6
7
8
9
func try() error {
var girl GirlGod
_db.Preload("Dogs").First(&girl)
fmt.Println(girl)
return nil
}
// output :
{{1 2024-05-13 22:08:48.663 +0800 CST 2024-05-13 22:08:48.663 +0800 CST {0001-01-01 00:00:00 +0000 UTC false}} 奇奇 [{{1 2024-05-13 22:08:48.696 +0800 CST 2024-05-13 22:08:48.696 +0800 CST {0001-01-01 00:00:00 +0000 UTC false}} 汪汪11 {{0 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} 0 0}} {{2 2024-05-13 22:08:48.696 +0800 CST 2024-05-13 22:08:48.696 +0800 CST {0001-01-01 00:00:00 +0000 UTC false}} 汪汪21 {{0 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} 0 0}}]}

可以发现,狗的信息并没有被查出来,都是0,0

因此我们需要修改一下:dagger:多加一个preload,注意一定是Dogs.Info

  • 普通查询

查询某个人的狗的信息

1
2
3
4
5
6
7
8
9
10
func try() error {
var girl GirlGod
_db.Preload("Dogs.Info").Preload("Dogs").First(&girl)
fmt.Println(girl)
return nil
}
// output:
{{1 2024-05-13 22:08:48.663 +0800 CST 2024-05-13 22:08:48.663 +0800 CST {0001-01-01 00:00:00 +0000 UTC false}} 奇奇 [{{1 2024-05-13 22:08:48.696 +0800 CST 2024-05-13 22:08:48.696 +0800 CST {0001-01-01 00:00:00 +0000 UTC false}} 汪汪11 {{1 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} 100 1}} {{2 2024-05-13 22:08:48.696 +0800 CST 2024-05-13 22:08:48.696 +0800 CST {0001-01-01 00:00:00 +0000 UTC false}} 汪汪21 {{2 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} 20000 2}}]}
// 不如精简一下
_db.Preload("Dogs.Info").First(&girl) 就可以实现
  • 进阶查询

现在我要查询某条狗的信息

名字叫汪汪1号的狗的信息

1
2
3
4
5
6
7
8
func try() error {
var girl GirlGod
_db.Preload("Dogs.Info").Preload("Dogs", "name = ?", "汪汪1号").First(&girl)
fmt.Println(girl)
return nil
}
// output :
{{1 2024-05-13 22:08:48.663 +0800 CST 2024-05-13 22:08:48.663 +0800 CST {0001-01-01 00:00:00 +0000 UTC false}} 奇奇 [{{1 2024-05-13 22:08:48.696 +0800 CST 2024-05-13 22:08:48.696 +0800 CST {0001-01-01 00:00:00 +0000 UTC false}} 汪汪11 {{1 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} 100 1}}]}
  • 再进阶查询

现在我要查询某条狗的某个特殊信息

钱比100元多的且名字叫汪汪1号的狗的信息

1
2
3
4
5
6
7
8
func try() error {
var girl GirlGod
_db.Preload("Dogs.Info", "money > 100").Preload("Dogs", "name = ?", "汪汪1号").First(&girl)
fmt.Println(girl)
return nil
}
// output :
// {{1 2024-05-13 22:08:48.663 +0800 CST 2024-05-13 22:08:48.663 +0800 CST {0001-01-01 00:00:00 +0000 UTC false}} 奇奇 [{{1 2024-05-13 22:08:48.696 +0800 CST 2024-05-13 22:08:48.696 +0800 CST {0001-01-01 00:00:00 +0000 UTC false}} 汪汪1号 1 {{0 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC {0001-01-01 00:00:00 +0000 UTC false}} 0 0}}]}

总结

查询条件只适用于 当前存在的那一层

Join 提前连表#

我觉得看下面一个代码就够了

表示girl要带出一个钱大于200的狗

钱跟狗是一对一关系,并且限定条件在Info上,因此使用Joins

1
2
3
4
5
6
7
8
func try() error {
var girl GirlGod
_db.Preload("Dogs", func(db *gorm.DB) *gorm.DB {
return db.Joins("Info").Where("money > 200")
}).First(&girl)
fmt.Println(girl)
return nil
}
  • 一对一

    Join只能适用于一对一的关系

  • 为什么不用Preload

    如果我适用Preload,我的当前结构体是Dogs,因此当前字段是没有money字段的,

    而我适用Joins的时候,属于提前连表,因此Info结构体的字段我也能够使用了

  • 总结

    感觉Joins更多的用于比如说我要获得A结构体的值,但是限定条件是B的条件,但是B的条件无法获得,所以需要使用Joins

Many 2 Many#


前面都会了,这个更简单

记得写外键,不然他不清楚中间表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Info struct {
gorm.Model
Money int
DogID uint
}

type Dog struct {
gorm.Model
Name string
GirlGods []GirlGod `gorm:"many2many:dog_girl_god"`
Info Info
}

type GirlGod struct {
gorm.Model
Name string
Dogs []Dog `gorm:"many2many:dog_girl_god"`
}

image-20240513225052798

不想写具体操作,拜拜

关联模式#