四大请求方式(2)- gin+gorm实现简单crud

写在最前面

刚学了 gorm ,想着完善一下之前的四大请求方式,加上数据库,做一个简单的 crud 。

连接数据库 – init

通过 mysql.Open(dsn) 来连接数据库,然后 gorm.Open() 用于初始化 GORM 数据库连接,当然也还有别的连接方式。

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
var DB *gorm.DB

func init() {
username := "root" //账号
password := "thisIsAPassword" //密码
host := "127.0.0.1" //数据库地址,可以是Ip或者域名
port := 3306 //数据库端口
Dbname := "crud_demo" //数据库名
timeout := "10s" //连接超时,10秒

// dsn userName:pwd@tcp(ip:port)/databaseName 这些是必须的
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=%s",
username, password, host, port, Dbname, timeout)

// 初始化 GORM 数据库连接 并设置一些选项
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
SingularTable: true,
NoLowerCase: true,
},
})
if err != nil {
fmt.Println("数据库连接失败,err=" + err.Error())
}

fmt.Println("创建成功: ", db)
DB = db

}

结构体和API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ArticleList 文章列表 简要信息
type ArticleList struct {
Author string `gorm:"type:varchar(10)" json:"author" binding:"required"`
Title string `gorm:"type:varchar(10)" json:"title" binding:"required"`
Brief string `gorm:"type:varchar(30)" json:"brief" binding:"required"`
}

// ArticleModel 全部内容
type ArticleModel struct {
gorm.Model // id creatTime uT dT
ArticleList
Content string `gorm:"type:varchar(1000)" json:"content" binding:"required"`
}

// Response 封装响应格式
type Response struct {
Code int `json:"code"` // 规定 200 -- 正常 400 -- err 399 -- warning
Date any `json:"date"`
Message string `json:"msg"`
}

这里把 ArticleList 从 ArticleModel 中拆分出来,作为 API 来用,接着往下看就知道了~


测试 – Main函数

先初始化了几条数据:

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
func main() {

// 添加模型
DB.Debug().AutoMigrate(&ArticleModel{})

//// 初始数据
DB.Create(&[]ArticleModel{
{
ArticleList: ArticleList{
Title: "go语言入门",
Brief: "本书是go语言入门基础 balabala...",
Author: "作者a",
},
Content: "第一章:认识go语言.............",
},
{
ArticleList: ArticleList{
Title: "c语言入门到入土",
Brief: "本书是c语言入门教程,面向入土",
Author: "作者b",
},
Content: "你的第一个C程序: printf(\"hello world\");",
},
{
ArticleList: ArticleList{
Title: "数据库从删库到跑路",
Brief: "本书教你如何从删库到跑路",
Author: "作者c",
},
Content: "没别的,直接跑路吧",
},
})

// 设置路由 和 请求方式

router := gin.Default()

router.GET("/articles/", _getList)
router.GET("/articles/:id", _getDetails)
router.POST("/articles/", _post)
router.PUT("/articles/:id", _put) // 居然不是 update
router.DELETE("/articles/:id", _delete)

err := router.Run(":80")
if err != nil {
return
}
}

文章列表

不设置条件,通过 Find() 查找所有内容,然后放入到 ArticleList 切片中,这个可以作为一个 api 来限制返回给结果哪些属性。
此处仅返回文章简要信息即可。

1
2
3
4
5
6
7
8
9
10
11
12
// 文章列表
func _getList(arg *gin.Context) {

var list []ArticleList // api
DB.Debug().Model(&ArticleModel{}).Find(&list)

if len(list) == 0 {
arg.JSON(http.StatusOK, Response{0, "内容为空", "无结果"})
} else {
arg.JSON(http.StatusOK, Response{http.StatusOK, list, "响应成功"})
}
}

文章详情

从请求中拿到 id 然后去数据库中查找,并通过 ret.RowsAffected == 0 来判断是否查找到对应文章。一开始我用 ret.Error != nil
作为判断,但测试时输入错误的 id 没有提示,还是会返回,不过返回的是空值。

p1-查询错误返回空值

控制台也没有提示:

1
2
获取到id: 9999
[0.626ms] [rows:0] SELECT * FROM `ArticleModel` WHERE id = '9999' AND `ArticleModel`.`DeletedAt` IS NULL

可见是未查找到时没有进入第一个分支。后来知道 Find 方法不会返回 gorm.ErrRecordNotFound 错误,当没有匹配的记录时,它将返回一个零值对象。
有两种解决方式: 1. 可以改用 First 2. 把条件判断改为 ret.RowsAffected == 0

修改后:

p2-修改查询部分的错误后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 文章详情
func _getDetails(arg *gin.Context) {

bookId := arg.Param("id")
fmt.Printf("获取到id: %v\n", bookId)

var article ArticleModel
ret := DB.Debug().Where("id = ?", bookId).Find(&article)

if ret.RowsAffected == 0 {
fmt.Println("未查找到id为", bookId, "的文章")
arg.JSON(http.StatusOK, Response{0, "内容为空", "查找失败,文章不存在!"})
} else {
arg.JSON(http.StatusOK, Response{http.StatusOK, article, "响应成功"})
}
}

增 & 改

常用的几种类型都可以直接 shouldBind() 来处理,所以删除了之前文章用的 _bindJson()


同前面一样用 ret.RowsAffected == 0 作为判断条件。

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
// 修改文章
func _put(arg *gin.Context) {
// 拿到id
bookId := arg.Param("id")
fmt.Printf("获取到id: %v\n", bookId)

var article ArticleModel
// 拿到修改后的文章内容
err := arg.ShouldBind(&article)
if err != nil {
fmt.Println(err)
arg.JSON(http.StatusBadRequest, Response{0, nil, "请求数据验证失败"})
return // 验证失败,直接返回
}

ret := DB.Debug().Where("id = ?", bookId).Updates(&article)
if ret.RowsAffected == 0 {
fmt.Println("不存在id为", bookId, "的文章")
arg.JSON(http.StatusOK, Response{0, "内容为空", "查找失败,文章不存在!"})
} else {
fmt.Println("id为", bookId, "文章已被修改")
arg.JSON(200, Response{http.StatusOK, article.UpdatedAt, "修改成功"})
}

}

// 删除文章
func _delete(arg *gin.Context) {
// 拿到 id
bookId := arg.Param("id")
fmt.Printf("获取到id: %v\n", bookId)

// 查找 id
var data ArticleModel
ret := DB.Debug().Where("id = ?", bookId).Find(&data)

if ret.RowsAffected == 0 {
fmt.Println("未查找到id为", bookId, "的文章")
arg.JSON(http.StatusOK, Response{0, "不存在该文章", "删除失败!"})
} else {
DB.Debug().Delete(&data) // 删除
deleteTime := data.DeletedAt
// 实际上,不会真的删除,只会标记一个删除时间
DB.Debug().Model(&ArticleModel{}).Select("DeletedAt").Where("id = ?", bookId).Find(&deleteTime)
arg.JSON(200, Response{http.StatusOK, gin.H{"删除时间": deleteTime}, "删除成功"})
fmt.Println("id为", bookId, "文章已被修改")
}

}
  • 一开始我设置的 Content 字段有默认值 default:"这个人很懒,没有写任何内容..." 但它又是一个必填项。

结果如下:

默认值和必填项冲突

实际上,由于接收请求是发生在将数据添加到数据库之前,所以默认值就不起作用了

  • 另外我犯的错误是在第 10 行的 if 分支里面,只是输出错误,而没有返回终止后面的程序,所以在绑定参数时,对于必填项但没有填的只会输出错误,然后继续将这条数据添加到数据库

在执行删除操作时,实际上,不会真的删除数据,只会标记一个删除时间,每次查找时,都会加上条件 DeletedAt IS NULL ,即没有标记过删除时间。

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
// 删除文章
func _delete(arg *gin.Context) {
// 拿到 id
bookId := arg.Param("id")
fmt.Printf("获取到id: %v\n", bookId)

// 查找 id
var data ArticleModel
ret := DB.Debug().Where("id = ?", bookId).Find(&data)

if ret.RowsAffected == 0 {
fmt.Println("未查找到id为", bookId, "的文章")
arg.JSON(http.StatusOK, Response{0, "不存在该文章", "删除失败!"})
} else {
DB.Debug().Delete(&data) // 删除
deleteTime := data.DeletedAt
// 实际上,不会真的删除,只会标记一个删除时间
DB.Debug().Model(&ArticleModel{}).Select("DeletedAt").Where("id = ?", bookId).Find(&deleteTime)
// `SELECT `DeletedAt` FROM `ArticleModel` WHERE id = '27' AND `ArticleModel`.`DeletedAt` IS NULL`

arg.JSON(200, Response{http.StatusOK, gin.H{"删除时间": deleteTime}, "删除成功"})
fmt.Println("id为", bookId, "文章已被修改")
}

}

写在最后

可以说这次简单的crud主要时间就花在修bug,各种各样的bug。`(>﹏<)′
算是大概修完了吧。等以后学了更多东西再迭代一次(写bug,修bug)。

再补一个图:

summer pockets

感谢夏日口袋陪伴我度过了这个夏天,夏日结束了,我的冒险还未结束~


四大请求方式(2)- gin+gorm实现简单crud
https://kjasn.github.io/2023/09/16/四大请求方式(2)-gin-gorm实现简单crud/
作者
Kjasn
发布于
2023年9月16日
许可协议