「连载四」Gin搭建Blog API's (三)

  1. 涉及知识点
  2. 本文目标
  3. 定义接口
  4. 编写路由逻辑
  5. 验证功能
  6. 参考
    1. 本系列示例代码
  7. 关于
    1. 修改记录
    1. 我的公众号

涉及知识点

本文目标

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

定义接口

本节编写文章的逻辑,我们定义一下接口吧!

  • 获取文章列表:GET(“/articles”)
  • 获取指定文章:POST(“/articles/:id”)
  • 新建文章:POST(“/articles”)
  • 更新指定文章:PUT(“/articles/:id”)
  • 删除指定文章:DELETE(“/articles/:id”)

编写路由逻辑

routers的 v1 版本下,新建article.go文件,写入内容:

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
package v1

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

//获取单个文章
func GetArticle(c *gin.Context) {
}

//获取多个文章
func GetArticles(c *gin.Context) {
}

//新增文章
func AddArticle(c *gin.Context) {
}

//修改文章
func EditArticle(c *gin.Context) {
}

//删除文章
func DeleteArticle(c *gin.Context) {
}

我们打开routers下的router.go文件,修改文件内容为:

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
package routers

import (
"github.com/gin-gonic/gin"

"github.com/EDDYCJY/go-gin-example/routers/api/v1"
"github.com/EDDYCJY/go-gin-example/pkg/setting"
)

func InitRouter() *gin.Engine {
...
apiv1 := r.Group("/api/v1")
{
...
//获取文章列表
apiv1.GET("/articles", v1.GetArticles)
//获取指定文章
apiv1.GET("/articles/:id", v1.GetArticle)
//新建文章
apiv1.POST("/articles", v1.AddArticle)
//更新指定文章
apiv1.PUT("/articles/:id", v1.EditArticle)
//删除指定文章
apiv1.DELETE("/articles/:id", v1.DeleteArticle)
}

return r
}

当前目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go-gin-example/
├── conf
│   └── app.ini
├── main.go
├── middleware
├── models
│   ├── models.go
│   └── tag.go
├── pkg
│   ├── e
│   │   ├── code.go
│   │   └── msg.go
│   ├── setting
│   │   └── setting.go
│   └── util
│   └── pagination.go
├── routers
│   ├── api
│   │   └── v1
│   │   ├── article.go
│   │   └── tag.go
│   └── router.go
├── runtime

在基础的路由规则配置结束后,我们开始编写我们的接口吧!


##编写 models 逻辑
创建models目录下的article.go,写入文件内容:

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
package models

import (
"github.com/jinzhu/gorm"

"time"
)

type Article struct {
Model

TagID int `json:"tag_id" gorm:"index"`
Tag Tag `json:"tag"`

Title string `json:"title"`
Desc string `json:"desc"`
Content string `json:"content"`
CreatedBy string `json:"created_by"`
ModifiedBy string `json:"modified_by"`
State int `json:"state"`
}


func (article *Article) BeforeCreate(scope *gorm.Scope) error {
scope.SetColumn("CreatedOn", time.Now().Unix())

return nil
}

func (article *Article) BeforeUpdate(scope *gorm.Scope) error {
scope.SetColumn("ModifiedOn", time.Now().Unix())

return nil
}

我们创建了一个Article struct {},与Tag不同的是,Article多了几项,如下:

  1. gorm:index,用于声明这个字段为索引,如果你使用了自动迁移功能则会有所影响,在不使用则无影响
  2. Tag字段,实际是一个嵌套的struct,它利用TagIDTag模型相互关联,在执行查询的时候,能够达到ArticleTag关联查询的功能
  3. time.Now().Unix() 返回当前的时间戳

接下来,请确保已对上一章节的内容通读且了解,由于逻辑偏差不会太远,我们本节直接编写这五个接口


打开models目录下的article.go,修改文件内容:

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
package models

import (
"time"

"github.com/jinzhu/gorm"
)

type Article struct {
Model

TagID int `json:"tag_id" gorm:"index"`
Tag Tag `json:"tag"`

Title string `json:"title"`
Desc string `json:"desc"`
Content string `json:"content"`
CreatedBy string `json:"created_by"`
ModifiedBy string `json:"modified_by"`
State int `json:"state"`
}


func ExistArticleByID(id int) bool {
var article Article
db.Select("id").Where("id = ?", id).First(&article)

if article.ID > 0 {
return true
}

return false
}

func GetArticleTotal(maps interface {}) (count int){
db.Model(&Article{}).Where(maps).Count(&count)

return
}

func GetArticles(pageNum int, pageSize int, maps interface {}) (articles []Article) {
db.Preload("Tag").Where(maps).Offset(pageNum).Limit(pageSize).Find(&articles)

return
}

func GetArticle(id int) (article Article) {
db.Where("id = ?", id).First(&article)
db.Model(&article).Related(&article.Tag)

return
}

func EditArticle(id int, data interface {}) bool {
db.Model(&Article{}).Where("id = ?", id).Updates(data)

return true
}

func AddArticle(data map[string]interface {}) bool {
db.Create(&Article {
TagID : data["tag_id"].(int),
Title : data["title"].(string),
Desc : data["desc"].(string),
Content : data["content"].(string),
CreatedBy : data["created_by"].(string),
State : data["state"].(int),
})

return true
}

func DeleteArticle(id int) bool {
db.Where("id = ?", id).Delete(Article{})

return true
}

func (article *Article) BeforeCreate(scope *gorm.Scope) error {
scope.SetColumn("CreatedOn", time.Now().Unix())

return nil
}

func (article *Article) BeforeUpdate(scope *gorm.Scope) error {
scope.SetColumn("ModifiedOn", time.Now().Unix())

return nil
}

在这里,我们拿出三点不同来讲,如下:

1、 我们的Article是如何关联到Tag

1
2
3
4
5
6
func GetArticle(id int) (article Article) {
db.Where("id = ?", id).First(&article)
db.Model(&article).Related(&article.Tag)

return
}

能够达到关联,首先是gorm本身做了大量的约定俗成

  • Article有一个结构体成员是TagID,就是外键。gorm会通过类名+ID 的方式去找到这两个类之间的关联关系
  • Article有一个结构体成员是Tag,就是我们嵌套在Article里的Tag结构体,我们可以通过Related进行关联查询

2、 Preload是什么东西,为什么查询可以得出每一项的关联Tag

1
2
3
4
5
func GetArticles(pageNum int, pageSize int, maps interface {}) (articles []Article) {
db.Preload("Tag").Where(maps).Offset(pageNum).Limit(pageSize).Find(&articles)

return
}

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
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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
package v1

import (
"net/http"
"log"

"github.com/gin-gonic/gin"
"github.com/astaxie/beego/validation"
"github.com/unknwon/com"

"github.com/EDDYCJY/go-gin-example/models"
"github.com/EDDYCJY/go-gin-example/pkg/e"
"github.com/EDDYCJY/go-gin-example/pkg/setting"
"github.com/EDDYCJY/go-gin-example/pkg/util"
)

//获取单个文章
func GetArticle(c *gin.Context) {
id := com.StrTo(c.Param("id")).MustInt()

valid := validation.Validation{}
valid.Min(id, 1, "id").Message("ID必须大于0")

code := e.INVALID_PARAMS
var data interface {}
if ! valid.HasErrors() {
if models.ExistArticleByID(id) {
data = models.GetArticle(id)
code = e.SUCCESS
} else {
code = e.ERROR_NOT_EXIST_ARTICLE
}
} else {
for _, err := range valid.Errors {
log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
}
}

c.JSON(http.StatusOK, gin.H{
"code" : code,
"msg" : e.GetMsg(code),
"data" : data,
})
}

//获取多个文章
func GetArticles(c *gin.Context) {
data := make(map[string]interface{})
maps := make(map[string]interface{})
valid := validation.Validation{}

var state int = -1
if arg := c.Query("state"); arg != "" {
state = com.StrTo(arg).MustInt()
maps["state"] = state

valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
}

var tagId int = -1
if arg := c.Query("tag_id"); arg != "" {
tagId = com.StrTo(arg).MustInt()
maps["tag_id"] = tagId

valid.Min(tagId, 1, "tag_id").Message("标签ID必须大于0")
}

code := e.INVALID_PARAMS
if ! valid.HasErrors() {
code = e.SUCCESS

data["lists"] = models.GetArticles(util.GetPage(c), setting.PageSize, maps)
data["total"] = models.GetArticleTotal(maps)

} else {
for _, err := range valid.Errors {
log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
}
}

c.JSON(http.StatusOK, gin.H{
"code" : code,
"msg" : e.GetMsg(code),
"data" : data,
})
}

//新增文章
func AddArticle(c *gin.Context) {
tagId := com.StrTo(c.Query("tag_id")).MustInt()
title := c.Query("title")
desc := c.Query("desc")
content := c.Query("content")
createdBy := c.Query("created_by")
state := com.StrTo(c.DefaultQuery("state", "0")).MustInt()

valid := validation.Validation{}
valid.Min(tagId, 1, "tag_id").Message("标签ID必须大于0")
valid.Required(title, "title").Message("标题不能为空")
valid.Required(desc, "desc").Message("简述不能为空")
valid.Required(content, "content").Message("内容不能为空")
valid.Required(createdBy, "created_by").Message("创建人不能为空")
valid.Range(state, 0, 1, "state").Message("状态只允许0或1")

code := e.INVALID_PARAMS
if ! valid.HasErrors() {
if models.ExistTagByID(tagId) {
data := make(map[string]interface {})
data["tag_id"] = tagId
data["title"] = title
data["desc"] = desc
data["content"] = content
data["created_by"] = createdBy
data["state"] = state

models.AddArticle(data)
code = e.SUCCESS
} else {
code = e.ERROR_NOT_EXIST_TAG
}
} else {
for _, err := range valid.Errors {
log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
}
}

c.JSON(http.StatusOK, gin.H{
"code" : code,
"msg" : e.GetMsg(code),
"data" : make(map[string]interface{}),
})
}

//修改文章
func EditArticle(c *gin.Context) {
valid := validation.Validation{}

id := com.StrTo(c.Param("id")).MustInt()
tagId := com.StrTo(c.Query("tag_id")).MustInt()
title := c.Query("title")
desc := c.Query("desc")
content := c.Query("content")
modifiedBy := c.Query("modified_by")

var state int = -1
if arg := c.Query("state"); arg != "" {
state = com.StrTo(arg).MustInt()
valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
}

valid.Min(id, 1, "id").Message("ID必须大于0")
valid.MaxSize(title, 100, "title").Message("标题最长为100字符")
valid.MaxSize(desc, 255, "desc").Message("简述最长为255字符")
valid.MaxSize(content, 65535, "content").Message("内容最长为65535字符")
valid.Required(modifiedBy, "modified_by").Message("修改人不能为空")
valid.MaxSize(modifiedBy, 100, "modified_by").Message("修改人最长为100字符")

code := e.INVALID_PARAMS
if ! valid.HasErrors() {
if models.ExistArticleByID(id) {
if models.ExistTagByID(tagId) {
data := make(map[string]interface {})
if tagId > 0 {
data["tag_id"] = tagId
}
if title != "" {
data["title"] = title
}
if desc != "" {
data["desc"] = desc
}
if content != "" {
data["content"] = content
}

data["modified_by"] = modifiedBy

models.EditArticle(id, data)
code = e.SUCCESS
} else {
code = e.ERROR_NOT_EXIST_TAG
}
} else {
code = e.ERROR_NOT_EXIST_ARTICLE
}
} else {
for _, err := range valid.Errors {
log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
}
}

c.JSON(http.StatusOK, gin.H{
"code" : code,
"msg" : e.GetMsg(code),
"data" : make(map[string]string),
})
}

//删除文章
func DeleteArticle(c *gin.Context) {
id := com.StrTo(c.Param("id")).MustInt()

valid := validation.Validation{}
valid.Min(id, 1, "id").Message("ID必须大于0")

code := e.INVALID_PARAMS
if ! valid.HasErrors() {
if models.ExistArticleByID(id) {
models.DeleteArticle(id)
code = e.SUCCESS
} else {
code = e.ERROR_NOT_EXIST_ARTICLE
}
} else {
for _, err := range valid.Errors {
log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
}
}

c.JSON(http.StatusOK, gin.H{
"code" : code,
"msg" : e.GetMsg(code),
"data" : make(map[string]string),
})
}

当前目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go-gin-example/
├── conf
│   └── app.ini
├── main.go
├── middleware
├── models
│   ├── article.go
│   ├── models.go
│   └── tag.go
├── pkg
│   ├── e
│   │   ├── code.go
│   │   └── msg.go
│   ├── setting
│   │   └── setting.go
│   └── util
│   └── pagination.go
├── routers
│   ├── api
│   │   └── v1
│   │   ├── article.go
│   │   └── tag.go
│   └── router.go
├── runtime

验证功能

我们重启服务,执行go run main.go,检查控制台输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ go run main.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET /api/v1/tags --> gin-blog/routers/api/v1.GetTags (3 handlers)
[GIN-debug] POST /api/v1/tags --> gin-blog/routers/api/v1.AddTag (3 handlers)
[GIN-debug] PUT /api/v1/tags/:id --> gin-blog/routers/api/v1.EditTag (3 handlers)
[GIN-debug] DELETE /api/v1/tags/:id --> gin-blog/routers/api/v1.DeleteTag (3 handlers)
[GIN-debug] GET /api/v1/articles --> gin-blog/routers/api/v1.GetArticles (3 handlers)
[GIN-debug] GET /api/v1/articles/:id --> gin-blog/routers/api/v1.GetArticle (3 handlers)
[GIN-debug] POST /api/v1/articles --> gin-blog/routers/api/v1.AddArticle (3 handlers)
[GIN-debug] PUT /api/v1/articles/:id --> gin-blog/routers/api/v1.EditArticle (3 handlers)
[GIN-debug] DELETE /api/v1/articles/:id --> gin-blog/routers/api/v1.DeleteArticle (3 handlers)

使用Postman检验接口是否正常,在这里大家可以选用合适的参数传递方式,此处为了方便展示我选用了 GET/Param 传参的方式,而后期会改为 POST。

至此,我们的 API’s 编写就到这里,下一节我们将介绍另外的一些技巧!

参考

本系列示例代码

关于

修改记录

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

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

我的公众号

image


免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 [email protected]

推荐阅读:

文章标题:「连载四」Gin搭建Blog API's (三)

本文作者:知识铺

发布时间:2018-02-13, 12:00:00

最后更新:2020-04-19, 17:52:49

原始链接:https://blog.zshipu.com/2018/02/13/golang/go/gin/2018-02-13-api-03/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏