本篇是对上一篇博客的继续补充(当然也可以视作独立的一篇)。
友情提示,本篇博客中用到了数据库可视化工具Navicat。另外,本篇博客的所有代码都可以从这里获取。
建立大致项目结构
Gorm是一个可以操作数据库的框架。为了更方便观察对数据库的操作,我们先建立一个基础从项目结构:
只有一个路由组,路由组里只有一个首页的路由。还要配置一下go mod的相关项。
先使用go mod创建一个项目,比如项目名称就叫gormnote。
go mod init gormnote
然后获取一下要用到的包。
go get github.com/gin-gonic/gin
go get gorm.io/driver/mysql
go get gorm.io/gorm
然后就要建立项目结构了:
和之前的一样,在main.go
中调用路由组。
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
中配置路由。
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
中实现路由逻辑。
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。
{{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
。- 其他部分一般不用改动。
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。
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
判断似乎并没有用(就忽略不计了)。
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
里添加这些。
<ul>
{{range $user := .userList}}
<li> {{$user}} </li>
{{end}}
</ul>
至此,基本项目结构就完成了。
数据库的操作
经典增删改查。
增加数据
先来看增加数据。
配置一个新的Add
路由。
func (con DefaultController) Add(ctx *gin.Context) {
// 增加数据
// 实例化一个结构体并将它添加到数据库里。
user := models.User{
Username: "hlry",
Age: 20,
Email: "[email protected]",
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
值,那么就会报错。
还要记得在路由组里配置路由。
func DefaultRoutersInit(r *gin.Engine) {
defaultRouters := r.Group("/")
{
defaultRouters.GET("/", defaults.DefaultController{}.Index)
defaultRouters.GET("/add", defaults.DefaultController{}.Add)
}
}
删除数据
配置路由。
有两种删除数据的方法。可以实例化结构体时直接指定数据的id。也可以实例一个空的结构体,并提供要删除的数据id。
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,
})
}
查询数据
前面两种操作其实一直在用查询数据,只不过是没有任何限制条件地查询所有数据,这里介绍一些条件查询。
配置路由(不配置也可以)。
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,
})
}
修改数据
修改数据的方法就是,先找到要修改的数据,再用结构体赋值的方法对其修改。
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 = "[email protected]"
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。因为每篇文章只能有一个分类,而一个分类可以包含多篇文章,所以可以说文章属于分类的一个实例。
那么我们需要这样定义两个结构体。
package models
type Article struct {
Id int
Title string
CategaryId int
ArticleCategary ArticleCategary `gorm:"foreignKey:CategaryId;references:Id"`
}
func (Article) TableName() string {
return "article"
}
package models
type ArticleCategary struct {
Id int
Categary string
}
func (ArticleCategary) TableName() string {
return "article_categary"
}
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()
括号里的内容要与结构体里的属性名一致,而不是与类型一致。
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 To
和Has One
到底有什么区别,除了描述的方向相反以外,其他的方面真看不出来猫腻儿,感觉是同一回事。
Has Many
前面是每篇文章都属于一个分类。假如现在要查询每个分类及分类下的每篇文章,那么这就是一对多。
我们需要在ArticleCategary
里再加上一个属性。
type ArticleCategary struct {
Id int
Categary string
Article []Article `gorm:"foreignKey:CategaryId;references:Id"`
}
这个意思就是用一个名为Article
的结构体切片去存储每一个Article
,先不说后面那一串。到这里,可能会有些疑问,Article
和ArticleCategary
互相嵌套,这样不会出问题吗?实践验证,最后的结果是这两个互相嵌套的属性最后会以空值结束。就比如,我查到了一篇文章,那么它的结果大概是这样的:
"Id":1,
"Title":"新闻111",
"CategaryId":1,
"ArticleCategary":{
"Id":1,
"Categary":"类别1",
"Article":null
}
可以看到,虽然整个Article
里最后又嵌套的一个Article
,但是这个Article
会直接被设置为空值。
同理,ArticleCategary
也大致一样。
"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
表里的id
去article
表里进行匹配,把和Article.CategaryId
也就是article
表里categary_id
值一样的文章拿出来存放到ArticleCategary.Article
切片中。再说简单点就是用文章分类的id去找这个类别下的所有文章。
Many To Many
这部分稍微有些复杂。
假设现在每篇文章又多了一个标签Tags
属性,其类型是Tags
类型的切片,也就是说,一篇文章可以有多个标签,那么我们又需要定义一下Tags
类型。同时,在定义Tags
类型时,也要考虑到一个标签可以给多个文章使用,也就是一个标签可以属于多个文章,那么在Tags
结构体里还要加上一个Article
属性,其类型是Article
类型的切片。
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
的定义。
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
表,如果我们想查询每一篇文章并附带着它们的标签,只有这些还不够。因为是多对多关系,不可能在数据库的article
和tags
这两个表里实现,还需要再引入第三张表,比如就叫article_tags
表。这个表有两个属性,一个是article_id
,也就是文章id,另一个是tags_id
也就是标签id。把所有的关联起来的文章标签的id存放在这张表里。这张表只有这两项属性,不需要拥有自己的id,因为它只是起到一个连接的作用,我们不会特定的要查询某个文章标签的关联组合。
把这张表也定义成结构体。
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以后再把整行数据作为结果放到结构体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
也是同样的方法。
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,
})
}
得到的结构大概长这样:
{
"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对数据库的基本操作大概就是这样。以后如果还有什么再补充吧。
标签:null,models,数据库,Gorm,gin,Article,Id,id From: https://www.cnblogs.com/luviichann/p/18018450