本篇是对上一篇博客的继续补充(当然也可以视作独立的一篇)。
友情提示,本篇博客中用到了数据库可视化工具Navicat。另外,本篇博客的所有代码都可以从这里获取。
建立大致项目结构
Gorm是一个可以操作数据库的框架。为了更方便观察对数据库的操作,我们先建立一个基础从项目结构:
只有一个路由组,路由组里只有一个首页的路由。还要配置一下go mod的相关项。
先使用go mod创建一个项目,比如项目名称就叫gormnote。
1 | go mod init gormnote |
然后获取一下要用到的包。
1 | go get github.com/gin-gonic/gin |
然后就要建立项目结构了:
和之前的一样,在main.go
中调用路由组。
1 | package main |
在routers/defaultRouters.go
中配置路由。
1 | package routers |
在controllers/default/defaultController.go
中实现路由逻辑。
1 | package defaults //注意这里的包名不要定义成default关键字,稍微区分一下。 |
如果只是观察数据库的变化,不需要再渲染一张网页,但为了讲究一点,我们再在templates/default中写一个简单的index.html。
1 | {{define "default/index.html"}} |
到这里,静态的网页基本结构就写好了,之后就要和数据库建立联系。
新建一个models/core.go
。
这个文件里包含了一个init
函数,里面是数据库的连接方法。其中DB
就是数据库。
至于那一长串dsn
:
- 第一个
root
是用户名。 - 第二个
root
也就是冒号后面那个是密码。这里简单起见我就都设置成了root
。 - 括号里那部分是ip和端口。
/
和?
中间的是数据库的名称,注意是数据库的名称,不是连接的名称。charset=
后面的是编码格式,好像一般都会用utf8mb4
。- 其他部分一般不用改动。
1 | package models |
有了数据库以后我们要定义结构体和数据库实现映射关系。
比如我们在数据库里创建了一个user
用户表,其中包含了这么几项属性:
- id,用户id,具有自增属性。
- username,用户名。
- age,用户年龄。
- email,邮箱。
- add_time,创建时间(是一个时间戳)。
那么我们可以在models
下创建user.go
,在里面定义结构体User。
1 | package models |
若要查询数据库中的内容,需要修改路由中的逻辑。由于数据并不是一个,所以我们查询全部数据时用一个切片来接收数据。models.DB.Find()
会返回一个DB结构体,其中有一项err
属性,不过后来我试验这里的err
判断似乎并没有用(就忽略不计了)。
1 | func (con DefaultController) Index(ctx *gin.Context) { |
现在数据被传到了前台,还要再渲染出来。这里可以使用range
语句。在body
里添加这些。
1 | <ul> |
至此,基本项目结构就完成了。
数据库的操作
经典增删改查。
增加数据
先来看增加数据。
配置一个新的Add
路由。
1 | func (con DefaultController) Add(ctx *gin.Context) { |
这里并不需要写Id
的值,因为它是可以自增,而且是主键。如果强行写了一个已经存在的id
值,那么就会报错。
还要记得在路由组里配置路由。
1 | func DefaultRoutersInit(r *gin.Engine) { |
删除数据
配置路由。
有两种删除数据的方法。可以实例化结构体时直接指定数据的id。也可以实例一个空的结构体,并提供要删除的数据id。
1 | func (con DefaultController) Delete(ctx *gin.Context) { |
查询数据
前面两种操作其实一直在用查询数据,只不过是没有任何限制条件地查询所有数据,这里介绍一些条件查询。
配置路由(不配置也可以)。
1 | func (con DefaultController) Query(ctx *gin.Context) { |
修改数据
修改数据的方法就是,先找到要修改的数据,再用结构体赋值的方法对其修改。
1 | func (con DefaultController) Update(ctx *gin.Context) { |
关联查询
Belongs To
现在在数据库里新增一些数据。article
,文章:
- id,文章id。
- title,文章标题。
- categary_id,文章分类对应的类别id。
article_categary
,文章分类:
- id,类别的id。
- categary,类别。
假如我们现在要查询每篇文章及其对应的id。因为每篇文章只能有一个分类,而一个分类可以包含多篇文章,所以可以说文章属于分类的一个实例。
那么我们需要这样定义两个结构体。
1 | package models |
1 | package models |
gorm:"foreignKey:CategaryId;references:Id"
这段代码是重写外键和引用,Gorm有默认值,但我们已经建立的数据库的键与默认值不太匹配,所以要自己重新(另外,我觉得还是自己声明比较好,虽然默认值也就是和表名有关,但毕竟要约束表名,而且被封装起来看不到。自己重新一眼就能看出到底是哪个和哪个相连)。意思就是,现在要查询文章及其分类,那么就用Article.CategaryId
和ArticleCategary.Id
进行连接。也就是说,在数据库中,对于article
表里的每一篇文章,我们都要用文章的分类的id即categary_id
去article_categary
表里进行匹配,匹配的就是和categary_id
相同的id
。说得再简单一点就是,拿到一篇文章以后,不急着将它返回,先拿着它的categary_id
值去article_categary
表里的id
属性里找一样的值,找到以后把文章和找到的这个id
对应的分类绑定在一起返回。
使用预加载查询到带分类的文章。要注意的是,Preload()
括号里的内容要与结构体里的属性名一致,而不是与类型一致。
1 | func (con DefaultController) Query_bt(ctx *gin.Context) { |
Has One
恕我无能,笔者搞了半天也没搞明白到底Belongs To
和Has One
到底有什么区别,除了描述的方向相反以外,其他的方面真看不出来猫腻儿,感觉是同一回事。
Has Many
前面是每篇文章都属于一个分类。假如现在要查询每个分类及分类下的每篇文章,那么这就是一对多。
我们需要在ArticleCategary
里再加上一个属性。
1 | type ArticleCategary struct { |
这个意思就是用一个名为Article
的结构体切片去存储每一个Article
,先不说后面那一串。到这里,可能会有些疑问,Article
和ArticleCategary
互相嵌套,这样不会出问题吗?实践验证,最后的结果是这两个互相嵌套的属性最后会以空值结束。就比如,我查到了一篇文章,那么它的结果大概是这样的:
1 | "Id":1, |
可以看到,虽然整个Article
里最后又嵌套的一个Article
,但是这个Article
会直接被设置为空值。
同理,ArticleCategary
也大致一样。
1 | "Id":1, |
然后再说gorm:"foreignKey:CategaryId;references:Id"
这一串东西。这和前面的Belongs To
很像,也是重新外键和引用。这里的意思就是,在数据库里查询article_categary
时,我们要用ArticleCategary.Id
也就是的article_categary
表里的id
去article
表里进行匹配,把和Article.CategaryId
也就是article
表里categary_id
值一样的文章拿出来存放到ArticleCategary.Article
切片中。再说简单点就是用文章分类的id去找这个类别下的所有文章。
Many To Many
这部分稍微有些复杂。
假设现在每篇文章又多了一个标签Tags
属性,其类型是Tags
类型的切片,也就是说,一篇文章可以有多个标签,那么我们又需要定义一下Tags
类型。同时,在定义Tags
类型时,也要考虑到一个标签可以给多个文章使用,也就是一个标签可以属于多个文章,那么在Tags
结构体里还要加上一个Article
属性,其类型是Article
类型的切片。
1 | package models |
我知道你看到了那一大串密密麻麻的字母,但先别管。我们还要再修改一下Article
的定义。
1 | type Article struct { |
那么现在,数据库里有了新的tags
表,如果我们想查询每一篇文章并附带着它们的标签,只有这些还不够。因为是多对多关系,不可能在数据库的article
和tags
这两个表里实现,还需要再引入第三张表,比如就叫article_tags
表。这个表有两个属性,一个是article_id
,也就是文章id,另一个是tags_id
也就是标签id。把所有的关联起来的文章标签的id存放在这张表里。这张表只有这两项属性,不需要拥有自己的id,因为它只是起到一个连接的作用,我们不会特定的要查询某个文章标签的关联组合。
把这张表也定义成结构体。
1 | package models |
接下来,我们的查询思路就是,对于article
表里的每一篇文章,我们用它的id在article_tags
表里找到和所有这篇文章id一样的article_id
。找到以后用那一行数据的tags_id
属性再去tags
表里找和这个tags_id
一样的id。找到对应的id以后再把整行数据作为结果放到结构体Article
的Tags
切片里。最后再把带有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
:将Article
和Tags
连接起来的结构体是ArticleTags
。这段代码的意思就是要用前面拿到的Article.Id
去找匹配的ArticleTags.ArticleId
。
先跳过References:Id
先看最后一部分。joinReferences:TagId
:在找到和Article.Id
去找匹配的ArticleTags.ArticleId
以后,要根据这个ArticleTags.ArticleId
找到对应的TagsId
。References:Id
:我们这里是希望找到Article
及其所有的Tags
,那么这段代码的意思就是用前面的TagsId
在Tags
里找到匹配的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 | func (con DefaultController) Query_mtm(ctx *gin.Context) { |
得到的结构大概长这样:
1 | { |
Gorm对数据库的基本操作大概就是这样。以后如果还有什么再补充吧。