3.4 Gin搭建Blog API's (三)

涉及知识点

本文目标

    完成博客的文章类接口定义和编写

定义接口

本节编写文章的逻辑,我们定义一下接口吧!
    获取文章列表:GET("/articles")
    获取指定文章:POST("/articles/:id")
    新建文章:POST("/articles")
    更新指定文章:PUT("/articles/:id")
    删除指定文章:DELETE("/articles/:id")

编写路由逻辑

routers的v1版本下,新建article.go文件,写入内容:
1
package v1
2
3
import (
4
"github.com/gin-gonic/gin"
5
)
6
7
//获取单个文章
8
func GetArticle(c *gin.Context) {
9
}
10
11
//获取多个文章
12
func GetArticles(c *gin.Context) {
13
}
14
15
//新增文章
16
func AddArticle(c *gin.Context) {
17
}
18
19
//修改文章
20
func EditArticle(c *gin.Context) {
21
}
22
23
//删除文章
24
func DeleteArticle(c *gin.Context) {
25
}
Copied!
我们打开routers下的router.go文件,修改文件内容为:
1
package routers
2
3
import (
4
"github.com/gin-gonic/gin"
5
6
"github.com/EDDYCJY/go-gin-example/routers/api/v1"
7
"github.com/EDDYCJY/go-gin-example/pkg/setting"
8
)
9
10
func InitRouter() *gin.Engine {
11
...
12
apiv1 := r.Group("/api/v1")
13
{
14
...
15
//获取文章列表
16
apiv1.GET("/articles", v1.GetArticles)
17
//获取指定文章
18
apiv1.GET("/articles/:id", v1.GetArticle)
19
//新建文章
20
apiv1.POST("/articles", v1.AddArticle)
21
//更新指定文章
22
apiv1.PUT("/articles/:id", v1.EditArticle)
23
//删除指定文章
24
apiv1.DELETE("/articles/:id", v1.DeleteArticle)
25
}
26
27
return r
28
}
Copied!
当前目录结构:
1
go-gin-example/
2
├── conf
3
│ └── app.ini
4
├── main.go
5
├── middleware
6
├── models
7
│ ├── models.go
8
│ └── tag.go
9
├── pkg
10
│ ├── e
11
│ │ ├── code.go
12
│ │ └── msg.go
13
│ ├── setting
14
│ │ └── setting.go
15
│ └── util
16
│ └── pagination.go
17
├── routers
18
│ ├── api
19
│ │ └── v1
20
│ │ ├── article.go
21
│ │ └── tag.go
22
│ └── router.go
23
├── runtime
Copied!
在基础的路由规则配置结束后,我们开始编写我们的接口吧!

编写models逻辑

创建models目录下的article.go,写入文件内容:
1
package models
2
3
import (
4
"github.com/jinzhu/gorm"
5
6
"time"
7
)
8
9
type Article struct {
10
Model
11
12
TagID int `json:"tag_id" gorm:"index"`
13
Tag Tag `json:"tag"`
14
15
Title string `json:"title"`
16
Desc string `json:"desc"`
17
Content string `json:"content"`
18
CreatedBy string `json:"created_by"`
19
ModifiedBy string `json:"modified_by"`
20
State int `json:"state"`
21
}
22
23
24
func (article *Article) BeforeCreate(scope *gorm.Scope) error {
25
scope.SetColumn("CreatedOn", time.Now().Unix())
26
27
return nil
28
}
29
30
func (article *Article) BeforeUpdate(scope *gorm.Scope) error {
31
scope.SetColumn("ModifiedOn", time.Now().Unix())
32
33
return nil
34
}
Copied!
我们创建了一个Article struct {},与Tag不同的是,Article多了几项,如下:
    1.
    gorm:index,用于声明这个字段为索引,如果你使用了自动迁移功能则会有所影响,在不使用则无影响
    2.
    Tag字段,实际是一个嵌套的struct,它利用TagIDTag模型相互关联,在执行查询的时候,能够达到ArticleTag关联查询的功能
    3.
    time.Now().Unix() 返回当前的时间戳
接下来,请确保已对上一章节的内容通读且了解,由于逻辑偏差不会太远,我们本节直接编写这五个接口
打开models目录下的article.go,修改文件内容:
1
package models
2
3
import (
4
"time"
5
6
"github.com/jinzhu/gorm"
7
)
8
9
type Article struct {
10
Model
11
12
TagID int `json:"tag_id" gorm:"index"`
13
Tag Tag `json:"tag"`
14
15
Title string `json:"title"`
16
Desc string `json:"desc"`
17
Content string `json:"content"`
18
CreatedBy string `json:"created_by"`
19
ModifiedBy string `json:"modified_by"`
20
State int `json:"state"`
21
}
22
23
24
func ExistArticleByID(id int) bool {
25
var article Article
26
db.Select("id").Where("id = ?", id).First(&article)
27
28
if article.ID > 0 {
29
return true
30
}
31
32
return false
33
}
34
35
func GetArticleTotal(maps interface {}) (count int){
36
db.Model(&Article{}).Where(maps).Count(&count)
37
38
return
39
}
40
41
func GetArticles(pageNum int, pageSize int, maps interface {}) (articles []Article) {
42
db.Preload("Tag").Where(maps).Offset(pageNum).Limit(pageSize).Find(&articles)
43
44
return
45
}
46
47
func GetArticle(id int) (article Article) {
48
db.Where("id = ?", id).First(&article)
49
db.Model(&article).Related(&article.Tag)
50
51
return
52
}
53
54
func EditArticle(id int, data interface {}) bool {
55
db.Model(&Article{}).Where("id = ?", id).Updates(data)
56
57
return true
58
}
59
60
func AddArticle(data map[string]interface {}) bool {
61
db.Create(&Article {
62
TagID : data["tag_id"].(int),
63
Title : data["title"].(string),
64
Desc : data["desc"].(string),
65
Content : data["content"].(string),
66
CreatedBy : data["created_by"].(string),
67
State : data["state"].(int),
68
})
69
70
return true
71
}
72
73
func DeleteArticle(id int) bool {
74
db.Where("id = ?", id).Delete(Article{})
75
76
return true
77
}
78
79
func (article *Article) BeforeCreate(scope *gorm.Scope) error {
80
scope.SetColumn("CreatedOn", time.Now().Unix())
81
82
return nil
83
}
84
85
func (article *Article) BeforeUpdate(scope *gorm.Scope) error {
86
scope.SetColumn("ModifiedOn", time.Now().Unix())
87
88
return nil
89
}
Copied!
在这里,我们拿出三点不同来讲,如下:
1、 我们的Article是如何关联到Tag
1
func GetArticle(id int) (article Article) {
2
db.Where("id = ?", id).First(&article)
3
db.Model(&article).Related(&article.Tag)
4
5
return
6
}
Copied!
能够达到关联,首先是gorm本身做了大量的约定俗成
    Article有一个结构体成员是TagID,就是外键。gorm会通过类名+ID的方式去找到这两个类之间的关联关系
    Article有一个结构体成员是Tag,就是我们嵌套在Article里的Tag结构体,我们可以通过Related进行关联查询
2、 Preload是什么东西,为什么查询可以得出每一项的关联Tag
1
func GetArticles(pageNum int, pageSize int, maps interface {}) (articles []Article) {
2
db.Preload("Tag").Where(maps).Offset(pageNum).Limit(pageSize).Find(&articles)
3
4
return
5
}
Copied!
Preload就是一个预加载器,它会执行两条SQL,分别是SELECT * FROM blog_articles;SELECT * FROM blog_tag WHERE id IN (1,2,3,4);,那么在查询出结构后,gorm内部处理对应的映射逻辑,将其填充到ArticleTag中,会特别方便,并且避免了循环查询
那么有没有别的办法呢,大致是两种
    gormJoin
    循环Related
综合之下,还是Preload更好,如果你有更优的方案,欢迎说一下 :)
3、 v.(I) 是什么?
v表示一个接口值,I表示接口类型。这个实际就是Golang中的类型断言,用于判断一个接口值的实际类型是否为某个类型,或一个非接口值的类型是否实现了某个接口类型
打开routers目录下v1版本的article.go文件,修改文件内容:
1
package v1
2
3
import (
4
"net/http"
5
"log"
6
7
"github.com/gin-gonic/gin"
8
"github.com/astaxie/beego/validation"
9
"github.com/unknwon/com"
10
11
"github.com/EDDYCJY/go-gin-example/models"
12
"github.com/EDDYCJY/go-gin-example/pkg/e"
13
"github.com/EDDYCJY/go-gin-example/pkg/setting"
14
"github.com/EDDYCJY/go-gin-example/pkg/util"
15
)
16
17
//获取单个文章
18
func GetArticle(c *gin.Context) {
19
id := com.StrTo(c.Param("id")).MustInt()
20
21
valid := validation.Validation{}
22
valid.Min(id, 1, "id").Message("ID必须大于0")
23
24
code := e.INVALID_PARAMS
25
var data interface {}
26
if ! valid.HasErrors() {
27
if models.ExistArticleByID(id) {
28
data = models.GetArticle(id)
29
code = e.SUCCESS
30
} else {
31
code = e.ERROR_NOT_EXIST_ARTICLE
32
}
33
} else {
34
for _, err := range valid.Errors {
35
log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
36
}
37
}
38
39
c.JSON(http.StatusOK, gin.H{
40
"code" : code,
41
"msg" : e.GetMsg(code),
42
"data" : data,
43
})
44
}
45
46
//获取多个文章
47
func GetArticles(c *gin.Context) {
48
data := make(map[string]interface{})
49
maps := make(map[string]interface{})
50
valid := validation.Validation{}
51
52
var state int = -1
53
if arg := c.Query("state"); arg != "" {
54
state = com.StrTo(arg).MustInt()
55
maps["state"] = state
56
57
valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
58
}
59
60
var tagId int = -1
61
if arg := c.Query("tag_id"); arg != "" {
62
tagId = com.StrTo(arg).MustInt()
63
maps["tag_id"] = tagId
64
65
valid.Min(tagId, 1, "tag_id").Message("标签ID必须大于0")
66
}
67
68
code := e.INVALID_PARAMS
69
if ! valid.HasErrors() {
70
code = e.SUCCESS
71
72
data["lists"] = models.GetArticles(util.GetPage(c), setting.PageSize, maps)
73
data["total"] = models.GetArticleTotal(maps)
74
75
} else {
76
for _, err := range valid.Errors {
77
log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
78
}
79
}
80
81
c.JSON(http.StatusOK, gin.H{
82
"code" : code,
83
"msg" : e.GetMsg(code),
84
"data" : data,
85
})
86
}
87
88
//新增文章
89
func AddArticle(c *gin.Context) {
90
tagId := com.StrTo(c.Query("tag_id")).MustInt()
91
title := c.Query("title")
92
desc := c.Query("desc")
93
content := c.Query("content")
94
createdBy := c.Query("created_by")
95
state := com.StrTo(c.DefaultQuery("state", "0")).MustInt()
96
97
valid := validation.Validation{}
98
valid.Min(tagId, 1, "tag_id").Message("标签ID必须大于0")
99
valid.Required(title, "title").Message("标题不能为空")
100
valid.Required(desc, "desc").Message("简述不能为空")
101
valid.Required(content, "content").Message("内容不能为空")
102
valid.Required(createdBy, "created_by").Message("创建人不能为空")
103
valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
104
105
code := e.INVALID_PARAMS
106
if ! valid.HasErrors() {
107
if models.ExistTagByID(tagId) {
108
data := make(map[string]interface {})
109
data["tag_id"] = tagId
110
data["title"] = title
111
data["desc"] = desc
112
data["content"] = content
113
data["created_by"] = createdBy
114
data["state"] = state
115
116
models.AddArticle(data)
117
code = e.SUCCESS
118
} else {
119
code = e.ERROR_NOT_EXIST_TAG
120
}
121
} else {
122
for _, err := range valid.Errors {
123
log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
124
}
125
}
126
127
c.JSON(http.StatusOK, gin.H{
128
"code" : code,
129
"msg" : e.GetMsg(code),
130
"data" : make(map[string]interface{}),
131
})
132
}
133
134
//修改文章
135
func EditArticle(c *gin.Context) {
136
valid := validation.Validation{}
137
138
id := com.StrTo(c.Param("id")).MustInt()
139
tagId := com.StrTo(c.Query("tag_id")).MustInt()
140
title := c.Query("title")
141
desc := c.Query("desc")
142
content := c.Query("content")
143
modifiedBy := c.Query("modified_by")
144
145
var state int = -1
146
if arg := c.Query("state"); arg != "" {
147
state = com.StrTo(arg).MustInt()
148
valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
149
}
150
151
valid.Min(id, 1, "id").Message("ID必须大于0")
152
valid.MaxSize(title, 100, "title").Message("标题最长为100字符")
153
valid.MaxSize(desc, 255, "desc").Message("简述最长为255字符")
154
valid.MaxSize(content, 65535, "content").Message("内容最长为65535字符")
155
valid.Required(modifiedBy, "modified_by").Message("修改人不能为空")
156
valid.MaxSize(modifiedBy, 100, "modified_by").Message("修改人最长为100字符")
157
158
code := e.INVALID_PARAMS
159
if ! valid.HasErrors() {
160
if models.ExistArticleByID(id) {
161
if models.ExistTagByID(tagId) {
162
data := make(map[string]interface {})
163
if tagId > 0 {
164
data["tag_id"] = tagId
165
}
166
if title != "" {
167
data["title"] = title
168
}
169
if desc != "" {
170
data["desc"] = desc
171
}
172
if content != "" {
173
data["content"] = content
174
}
175
176
data["modified_by"] = modifiedBy
177
178
models.EditArticle(id, data)
179
code = e.SUCCESS
180
} else {
181
code = e.ERROR_NOT_EXIST_TAG
182
}
183
} else {
184
code = e.ERROR_NOT_EXIST_ARTICLE
185
}
186
} else {
187
for _, err := range valid.Errors {
188
log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
189
}
190
}
191
192
c.JSON(http.StatusOK, gin.H{
193
"code" : code,
194
"msg" : e.GetMsg(code),
195
"data" : make(map[string]string),
196
})
197
}
198
199
//删除文章
200
func DeleteArticle(c *gin.Context) {
201
id := com.StrTo(c.Param("id")).MustInt()
202
203
valid := validation.Validation{}
204
valid.Min(id, 1, "id").Message("ID必须大于0")
205
206
code := e.INVALID_PARAMS
207
if ! valid.HasErrors() {
208
if models.ExistArticleByID(id) {
209
models.DeleteArticle(id)
210
code = e.SUCCESS
211
} else {
212
code = e.ERROR_NOT_EXIST_ARTICLE
213
}
214
} else {
215
for _, err := range valid.Errors {
216
log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
217
}
218
}
219
220
c.JSON(http.StatusOK, gin.H{
221
"code" : code,
222
"msg" : e.GetMsg(code),
223
"data" : make(map[string]string),
224
})
225
}
Copied!
当前目录结构:
1
go-gin-example/
2
├── conf
3
│ └── app.ini
4
├── main.go
5
├── middleware
6
├── models
7
│ ├── article.go
8
│ ├── models.go
9
│ └── tag.go
10
├── pkg
11
│ ├── e
12
│ │ ├── code.go
13
│ │ └── msg.go
14
│ ├── setting
15
│ │ └── setting.go
16
│ └── util
17
│ └── pagination.go
18
├── routers
19
│ ├── api
20
│ │ └── v1
21
│ │ ├── article.go
22
│ │ └── tag.go
23
│ └── router.go
24
├── runtime
Copied!

验证功能

我们重启服务,执行go run main.go,检查控制台输出结果
1
$ go run main.go
2
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
3
- using env: export GIN_MODE=release
4
- using code: gin.SetMode(gin.ReleaseMode)
5
6
[GIN-debug] GET /api/v1/tags --> gin-blog/routers/api/v1.GetTags (3 handlers)
7
[GIN-debug] POST /api/v1/tags --> gin-blog/routers/api/v1.AddTag (3 handlers)
8
[GIN-debug] PUT /api/v1/tags/:id --> gin-blog/routers/api/v1.EditTag (3 handlers)
9
[GIN-debug] DELETE /api/v1/tags/:id --> gin-blog/routers/api/v1.DeleteTag (3 handlers)
10
[GIN-debug] GET /api/v1/articles --> gin-blog/routers/api/v1.GetArticles (3 handlers)
11
[GIN-debug] GET /api/v1/articles/:id --> gin-blog/routers/api/v1.GetArticle (3 handlers)
12
[GIN-debug] POST /api/v1/articles --> gin-blog/routers/api/v1.AddArticle (3 handlers)
13
[GIN-debug] PUT /api/v1/articles/:id --> gin-blog/routers/api/v1.EditArticle (3 handlers)
14
[GIN-debug] DELETE /api/v1/articles/:id --> gin-blog/routers/api/v1.DeleteArticle (3 handlers)
Copied!
使用Postman检验接口是否正常,在这里大家可以选用合适的参数传递方式,此处为了方便展示我选用了 GET/Param 传参的方式,而后期会改为 POST。
至此,我们的API's编写就到这里,下一节我们将介绍另外的一些技巧!

参考

本系列示例代码

关于

修改记录

    第一版:2018年02月16日发布文章
    第二版:2019年10月01日修改文章

如果有任何疑问或错误,欢迎在 issues 进行提问或给予修正意见,如果喜欢或对你有所帮助,欢迎 Star,对作者是一种鼓励和推进。

我的公众号

image
Last modified 2yr ago