少女祈祷中...

本篇是对上一篇博客的继续补充(当然也可以视作独立的一篇)。

友情提示,本篇博客中用到了数据库可视化工具Navicat。另外,本篇博客的所有代码都可以从这里获取。

建立大致项目结构

Gorm是一个可以操作数据库的框架。为了更方便观察对数据库的操作,我们先建立一个基础从项目结构:
只有一个路由组,路由组里只有一个首页的路由。还要配置一下go mod的相关项。

先使用go mod创建一个项目,比如项目名称就叫gormnote。

1
go mod init gormnote

然后获取一下要用到的包。

1
2
3
go get github.com/gin-gonic/gin
go get gorm.io/driver/mysql
go get gorm.io/gorm

然后就要建立项目结构了:
和之前的一样,在main.go中调用路由组。

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

import (
"gormnote/routers"

"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()
r.LoadHTMLGlob("templates/**/*")
routers.DefaultRoutersInit(r)
r.Run()
}

// github.com/pilu/fresh

routers/defaultRouters.go中配置路由。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package routers

import (
"gormnote/controllers/defaults"

"github.com/gin-gonic/gin"
)

func DefaultRoutersInit(r *gin.Engine) {
defaultRouters := r.Group("/")
{
defaultRouters.GET("/", defaults.DefaultController{}.Index)
}
}

controllers/default/defaultController.go中实现路由逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package defaults //注意这里的包名不要定义成default关键字,稍微区分一下。

import (
"net/http"

"github.com/gin-gonic/gin"
)

type DefaultController struct {
}

func (con DefaultController) Index(ctx *gin.Context) {
// ctx.String(http.StatusOK, "首页")
ctx.HTML(http.StatusOK, "default/index.html", gin.H{})
}

如果只是观察数据库的变化,不需要再渲染一张网页,但为了讲究一点,我们再在templates/default中写一个简单的index.html。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{{define "default/index.html"}}

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h2>首页</h2>
</body>
</html>
{{end}}

到这里,静态的网页基本结构就写好了,之后就要和数据库建立联系。

新建一个models/core.go
这个文件里包含了一个init函数,里面是数据库的连接方法。其中DB就是数据库。
至于那一长串dsn

  • 第一个root是用户名。
  • 第二个root也就是冒号后面那个是密码。这里简单起见我就都设置成了root
  • 括号里那部分是ip和端口。
  • /?中间的是数据库的名称,注意是数据库的名称,不是连接的名称。
  • charset=后面的是编码格式,好像一般都会用utf8mb4
  • 其他部分一般不用改动。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package models

import (
"fmt"

"gorm.io/driver/mysql"
"gorm.io/gorm"
)

var DB *gorm.DB
var err error

func init() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "root:root@tcp(127.0.0.1:3306)/gogin?charset=utf8mb4&parseTime=True&loc=Local"
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})

if err != nil {
fmt.Printf("err: %v\n", err)
fmt.Println("数据库连接失败!")
} else {
fmt.Println("数据库连接成功!")
}
}

有了数据库以后我们要定义结构体和数据库实现映射关系。
比如我们在数据库里创建了一个user用户表,其中包含了这么几项属性:

  • id,用户id,具有自增属性。
  • username,用户名。
  • age,用户年龄。
  • email,邮箱。
  • add_time,创建时间(是一个时间戳)。

那么我们可以在models下创建user.go,在里面定义结构体User。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package models

type User struct {
Id int
Username string
Age int
Email string
AddTime int
}

// 结构体对应的表明,Gorm给了默认情况的表名,这里是自定义表名。
// 意思是User这个结构体连接的数据表名字是user。
func (User) TableName() string {
return "user"
}

若要查询数据库中的内容,需要修改路由中的逻辑。由于数据并不是一个,所以我们查询全部数据时用一个切片来接收数据。
models.DB.Find()会返回一个DB结构体,其中有一项err属性,不过后来我试验这里的err判断似乎并没有用(就忽略不计了)。

1
2
3
4
5
6
7
8
9
10
11
func (con DefaultController) Index(ctx *gin.Context) {
// ctx.String(http.StatusOK, "首页")
userList := []models.User{} //定义一个User类型的切片。
if err := models.DB.Find(&userList).Error; err != nil {
fmt.Printf("err: %v\n", err)
}

ctx.HTML(http.StatusOK, "default/index.html", gin.H{
"userList": userList,
})
}

现在数据被传到了前台,还要再渲染出来。这里可以使用range语句。在body里添加这些。

1
2
3
4
5
<ul>
{{range $user := .userList}}
<li> {{$user}} </li>
{{end}}
</ul>

至此,基本项目结构就完成了。

数据库的操作

经典增删改查。

增加数据

先来看增加数据。
配置一个新的Add路由。

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
func (con DefaultController) Add(ctx *gin.Context) {
// 增加数据
// 实例化一个结构体并将它添加到数据库里。
user := models.User{
Username: "hlry",
Age: 20,
Email: "hlry@gmail.com",
AddTime: 1708060539,
}
if err := models.DB.Create(&user).Error; err != nil {
fmt.Println("数据添加失败!")
fmt.Printf("err: %v\n", err)
} else {
fmt.Println("数据添加成功!")
}

// 查询数据,验证一下确实是添加进去了。
userList := []models.User{}
if err := models.DB.Find(&userList).Error; err != nil {
fmt.Printf("err: %v\n", err)
}

ctx.HTML(http.StatusOK, "default/index.html", gin.H{
"userList": userList,
})
}

这里并不需要写Id的值,因为它是可以自增,而且是主键。如果强行写了一个已经存在的id值,那么就会报错。

还要记得在路由组里配置路由。

1
2
3
4
5
6
7
func DefaultRoutersInit(r *gin.Engine) {
defaultRouters := r.Group("/")
{
defaultRouters.GET("/", defaults.DefaultController{}.Index)
defaultRouters.GET("/add", defaults.DefaultController{}.Add)
}
}

删除数据

配置路由。
有两种删除数据的方法。可以实例化结构体时直接指定数据的id。也可以实例一个空的结构体,并提供要删除的数据id。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (con DefaultController) Delete(ctx *gin.Context) {
// 删除数据
// user := models.User{Id: 2} //指定删除的数据的id。
// models.DB.Delete(&user)

user := models.User{}
models.DB.Where("id = ?", 2).Delete(&user) //查询到id=2的数据并删除。

userList := []models.User{}
if err := models.DB.Find(&userList).Error; err != nil {
fmt.Printf("err: %v\n", err)
}

ctx.HTML(http.StatusOK, "default/index.html", gin.H{
"userList": userList,
})
}

查询数据

前面两种操作其实一直在用查询数据,只不过是没有任何限制条件地查询所有数据,这里介绍一些条件查询。
配置路由(不配置也可以)。

1
2
3
4
5
6
7
8
9
10
11
12
func (con DefaultController) Query(ctx *gin.Context) {
userList := []models.User{}
// 查询id>2的数据。
// models.DB.Where("id > ?", 2).Find(&userList)

// 查询id=3,4,6的数据,?是占位符,用切片打包要查询的数据的id。
models.DB.Where("id in ?", []int{3, 4, 6}).Find(&userList)

ctx.HTML(http.StatusOK, "default/index.html", gin.H{
"userList": userList,
})
}

修改数据

修改数据的方法就是,先找到要修改的数据,再用结构体赋值的方法对其修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (con DefaultController) Update(ctx *gin.Context) {
user := models.User{} //先实例化一个空结构体。
models.DB.Where("id = 4").Find(&user) //找到要修改的数据。
//修改数据。
user.Username = "kzh"
user.Age = 19
user.Email = "kzh@outlook.com"
models.DB.Save(&user)

userList := []models.User{}
if err := models.DB.Find(&userList).Error; err != nil {
fmt.Printf("err: %v\n", err)
}

ctx.HTML(http.StatusOK, "default/index.html", gin.H{
"userList": userList,
})
}

关联查询

Belongs To

现在在数据库里新增一些数据。
article,文章:

  • id,文章id。
  • title,文章标题。
  • categary_id,文章分类对应的类别id。

article_categary,文章分类:

  • id,类别的id。
  • categary,类别。

假如我们现在要查询每篇文章及其对应的id。因为每篇文章只能有一个分类,而一个分类可以包含多篇文章,所以可以说文章属于分类的一个实例。

那么我们需要这样定义两个结构体。

1
2
3
4
5
6
7
8
9
10
11
12
package models

type Article struct {
Id int
Title string
CategaryId int
ArticleCategary ArticleCategary `gorm:"foreignKey:CategaryId;references:Id"`
}

func (Article) TableName() string {
return "article"
}
1
2
3
4
5
6
7
8
9
10
package models

type ArticleCategary struct {
Id int
Categary string
}

func (ArticleCategary) TableName() string {
return "article_categary"
}

gorm:"foreignKey:CategaryId;references:Id"这段代码是重写外键和引用,Gorm有默认值,但我们已经建立的数据库的键与默认值不太匹配,所以要自己重新(另外,我觉得还是自己声明比较好,虽然默认值也就是和表名有关,但毕竟要约束表名,而且被封装起来看不到。自己重新一眼就能看出到底是哪个和哪个相连)。意思就是,现在要查询文章及其分类,那么就用Article.CategaryIdArticleCategary.Id进行连接。也就是说,在数据库中,对于article表里的每一篇文章,我们都要用文章的分类的id即categary_idarticle_categary表里进行匹配,匹配的就是和categary_id相同的id。说得再简单一点就是,拿到一篇文章以后,不急着将它返回,先拿着它的categary_id值去article_categary表里的id属性里找一样的值,找到以后把文章和找到的这个id对应的分类绑定在一起返回。

使用预加载查询到带分类的文章。要注意的是,Preload()括号里的内容要与结构体里的属性名一致,而不是与类型一致。

1
2
3
4
5
6
7
8
9
func (con DefaultController) Query_bt(ctx *gin.Context) {

articleList := []models.Article{}
models.DB.Preload("ArticleCategary").Find(&articleList)

ctx.HTML(http.StatusOK, "default/article.html", gin.H{
"results": articleList,
})
}

Has One

恕我无能,笔者搞了半天也没搞明白到底Belongs ToHas One到底有什么区别,除了描述的方向相反以外,其他的方面真看不出来猫腻儿,感觉是同一回事。

Has Many

前面是每篇文章都属于一个分类。假如现在要查询每个分类及分类下的每篇文章,那么这就是一对多。
我们需要在ArticleCategary里再加上一个属性。

1
2
3
4
5
type ArticleCategary struct {
Id int
Categary string
Article []Article `gorm:"foreignKey:CategaryId;references:Id"`
}

这个意思就是用一个名为Article的结构体切片去存储每一个Article,先不说后面那一串。到这里,可能会有些疑问,ArticleArticleCategary互相嵌套,这样不会出问题吗?实践验证,最后的结果是这两个互相嵌套的属性最后会以空值结束。就比如,我查到了一篇文章,那么它的结果大概是这样的:

1
2
3
4
5
6
7
8
"Id":1,
"Title":"新闻111",
"CategaryId":1,
"ArticleCategary":{
"Id":1,
"Categary":"类别1",
"Article":null
}

可以看到,虽然整个Article里最后又嵌套的一个Article,但是这个Article会直接被设置为空值。
同理,ArticleCategary也大致一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"Id":1,
"Categary":"类别1",
"Article":[
{
"Id":1,
"Title":"新闻111",
"CategaryId":1,
"ArticleCategary":{
"Id":0,
"Categary":"",
"Article":null
}
},
{
"Id":2,
"Title":"title222",
"CategaryId":1,
"ArticleCategary":{
"Id":0,
"Categary":"",
"Article":null
}
}
]

然后再说gorm:"foreignKey:CategaryId;references:Id"这一串东西。这和前面的Belongs To很像,也是重新外键和引用。这里的意思就是,在数据库里查询article_categary时,我们要用ArticleCategary.Id也就是的article_categary表里的idarticle表里进行匹配,把和Article.CategaryId也就是article表里categary_id值一样的文章拿出来存放到ArticleCategary.Article切片中。再说简单点就是用文章分类的id去找这个类别下的所有文章。

Many To Many

这部分稍微有些复杂。
假设现在每篇文章又多了一个标签Tags属性,其类型是Tags类型的切片,也就是说,一篇文章可以有多个标签,那么我们又需要定义一下Tags类型。同时,在定义Tags类型时,也要考虑到一个标签可以给多个文章使用,也就是一个标签可以属于多个文章,那么在Tags结构体里还要加上一个Article属性,其类型是Article类型的切片。

1
2
3
4
5
6
7
8
9
10
11
package models

type Tags struct {
Id int
Tag string
Article []Article `gorm:"many2many:article_tags;foreignKey:Id;joinForeignKey:TagID;References:Id;joinReferences:ArticleId"`
}

func (Tags) TableName() string {
return "tags"
}

我知道你看到了那一大串密密麻麻的字母,但先别管。我们还要再修改一下Article的定义。

1
2
3
4
5
6
7
type Article struct {
Id int
Title string
CategaryId int
ArticleCategary ArticleCategary `gorm:"foreignKey:CategaryId;references:Id"`
Tags []Tags `gorm:"many2many:article_tags;foreignKey:Id;joinForeignKey:ArticleID;References:Id;joinReferences:TagId"`
}

那么现在,数据库里有了新的tags表,如果我们想查询每一篇文章并附带着它们的标签,只有这些还不够。因为是多对多关系,不可能在数据库的articletags这两个表里实现,还需要再引入第三张表,比如就叫article_tags表。这个表有两个属性,一个是article_id,也就是文章id,另一个是tags_id也就是标签id。把所有的关联起来的文章标签的id存放在这张表里。这张表只有这两项属性,不需要拥有自己的id,因为它只是起到一个连接的作用,我们不会特定的要查询某个文章标签的关联组合。

把这张表也定义成结构体。

1
2
3
4
5
6
7
8
9
10
package models

type ArticleTags struct {
ArticleId int
TagId int
}

func (ArticleTags) TableName() string {
return "article_tags"
}

接下来,我们的查询思路就是,对于article表里的每一篇文章,我们用它的id在article_tags表里找到和所有这篇文章id一样的article_id。找到以后用那一行数据的tags_id属性再去tags表里找和这个tags_id一样的id。找到对应的id以后再把整行数据作为结果放到结构体ArticleTags切片里。最后再把带有Tags切片的Article作为查询结果返回。
思路有了,那么问题就是该怎么让几个结构体或者说数据表关联起来。这就需要解释一下那一大串字母了。
gorm:"many2many:article_tags;foreignKey:Id;joinForeignKey:ArticleID;References:Id;joinReferences:TagId"
这里也是像之前一样重写了外键和引用。

  • many2many:article_tags:意思是连接名为article_tags的数据表,我们要拿哪个数据表做中间连接就用哪个表的名称。
  • foreignKey:Id:我们这里是要查询article,所有的重写都是定义在Article结构体里的。这段代码意思就是要用Article里的Id去连接ArticleTags结构体或者说article_tags表。
  • joinForeignKey:ArticleID:将ArticleTags连接起来的结构体是ArticleTags。这段代码的意思就是要用前面拿到的Article.Id去找匹配的ArticleTags.ArticleId
    先跳过References:Id先看最后一部分。
  • joinReferences:TagId:在找到和Article.Id去找匹配的ArticleTags.ArticleId以后,要根据这个ArticleTags.ArticleId找到对应的TagsId
  • References:Id:我们这里是希望找到Article及其所有的Tags,那么这段代码的意思就是用前面的TagsIdTags里找到匹配的Tags.Id
    最后,因为Tags.Id就是Tags结构体或者说tags表的主键,所以可以找到对应的标签,并将整个数据存入Article.[]Tags里。

总结一下就是:

  • many2many就是连接两个多对多关系的表(简单起见,以下就叫A和B,并且要查询A及其所带的B属性)。
  • foreignKey就是A表里要拿去在连接表里找一样的值的属性。
  • joinForeignKey就是要去和A表里拿过来的那条属性匹配的属性。
  • joinReferences就是找到连接表里那条相匹配的数据以后,需要用它的值去B表里查询的属性。
  • References:Id就是要被查询的值在B表里查询的那个属性。

整个流程就是:

  • A -> A.foreignKey
  • A.foreignKey -> A_B.joinForeignKey
  • A_B.joinForeignKey -> A_B.joinReferences
  • A_B.joinReferences -> B.References
  • B.References -> B
    这里起始的A和结束的B分别代指A表和B表里的一条或多条数据。

解释完上面这些,就可以直接查询了。查询Tags及其所拥有的Article也是同样的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (con DefaultController) Query_mtm(ctx *gin.Context) {
articleList := []models.Article{}
models.DB.Preload("Tags").Find(&articleList)
// ctx.HTML(http.StatusOK, "default/article.html", gin.H{
// "results": articleList,
// })
ctx.JSON(http.StatusOK, gin.H{
"articleList": articleList,
})

tagsList := []models.Tags{}
models.DB.Preload("Article").Find(&tagsList)
// ctx.HTML(http.StatusOK, "default/article.html", gin.H{
// "results": tagsList,
// })
ctx.JSON(http.StatusOK, gin.H{
"tagsList": tagsList,
})
}

得到的结构大概长这样:

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
{
"articleList":[
{
"Id":1,
"Title":"新闻111",
"CategaryId":1,
"ArticleCategary":{
"Id":0,
"Categary":"",
"Article":null
},
"Tags":[
{
"Id":1,
"Tag":"tag111",
"Article":null
},
{
"Id":2,
"Tag":"tag222",
"Article":null
},
{
"Id":5,
"Tag":"tag555",
"Article":null
}
]
},
{
"Id":2,
"Title":"title222",
"CategaryId":1,
"ArticleCategary":{
"Id":0,
"Categary":"",
"Article":null
},
"Tags":[
{
"Id":1,
"Tag":"tag111",
"Article":null
},
{
"Id":3,
"Tag":"tag333",
"Article":null
}
]
},
{
"Id":3,
"Title":"title333",
"CategaryId":2,
"ArticleCategary":{
"Id":0,
"Categary":"",
"Article":null
},
"Tags":[
{
"Id":5,
"Tag":"tag555",
"Article":null
},
{
"Id":6,
"Tag":"tag666",
"Article":null
}
]
},
{
"Id":4,
"Title":"title444",
"CategaryId":3,
"ArticleCategary":{
"Id":0,
"Categary":"",
"Article":null
},
"Tags":[
{
"Id":2,
"Tag":"tag222",
"Article":null
},
{
"Id":3,
"Tag":"tag333",
"Article":null
},
{
"Id":4,
"Tag":"tag444",
"Article":null
}
]
},
{
"Id":5,
"Title":"title555",
"CategaryId":4,
"ArticleCategary":{
"Id":0,
"Categary":"",
"Article":null
},
"Tags":[
{
"Id":1,
"Tag":"tag111",
"Article":null
},
{
"Id":2,
"Tag":"tag222",
"Article":null
},
{
"Id":5,
"Tag":"tag555",
"Article":null
},
{
"Id":6,
"Tag":"tag666",
"Article":null
}
]
},
{
"Id":6,
"Title":"title666",
"CategaryId":3,
"ArticleCategary":{
"Id":0,
"Categary":"",
"Article":null
},
"Tags":[
{
"Id":1,
"Tag":"tag111",
"Article":null
},
{
"Id":2,
"Tag":"tag222",
"Article":null
},
{
"Id":6,
"Tag":"tag666",
"Article":null
}
]
},
{
"Id":7,
"Title":"title777",
"CategaryId":4,
"ArticleCategary":{
"Id":0,
"Categary":"",
"Article":null
},
"Tags":[
{
"Id":1,
"Tag":"tag111",
"Article":null
},
{
"Id":3,
"Tag":"tag333",
"Article":null
}
]
},
{
"Id":8,
"Title":"title888",
"CategaryId":2,
"ArticleCategary":{
"Id":0,
"Categary":"",
"Article":null
},
"Tags":[
{
"Id":1,
"Tag":"tag111",
"Article":null
},
{
"Id":2,
"Tag":"tag222",
"Article":null
}
]
}
]
}

Gorm对数据库的基本操作大概就是这样。以后如果还有什么再补充吧。