【Go Web开发】PUT请求处理

上一篇文章我们介绍了GET请求处理,在本节我们将继续构建我们的应用程序,并添加一个全新的API接口,允许客户端更新特定mvoie数据。

Method URL Pattern Handler 操作
PUT /v1/movies/:id updateMovieHandler 更新特定movie信息

更准确地说,我们将添加接口,以便客户端可以更新数据库中movie的title、year、runtime和genres内容。在我们的项目中,id和created_at一旦创建它们就不应该改变,并且版本值不应该由客户端控制,所以不允许编辑这些字段。

现在,我们将配置这个接口,使它对movie的值进行替换。这意味着客户端需要在其JSON请求体中为所有可编辑字段提供值……即使他们只想改变其中一个字段。

例如,如果用户想要在数据库中添加科幻电影《黑豹》,需要发送一个JSON请求体,如下所示:

{
    "title": "Black Panther", 
    "year": 2018,
    "runtime": "134 mins", 
    "genres": [
        "action",
        "adventure",
        "sci-fi"
    ] 
}

执行SQL查询

让我们再次开始数据库模型处理,并编辑Update()方法来执行下面SQL语句:

UPDATE movies
SET title = $1, year = $2, runtime = $3, genres = $4, version = version + 1 
WHERE id = $5
RETURNING version

注意到这里我们将版本值作为查询的一部分进行递增,最后我们用return子句返回这个加1后的版本值。

和前面一样这个SQL语句返回一条数据,因此我们需要使用Go的QueryRow()方法执行。如果你跟随本系列文章操作,返回到internal/data/movies.go文件,然后在Update方法中添加如下代码:
File: internal/data/movies.go


package data

...

func (m MovieModel) Update(movie *Movie) error {
    //声明SQL更新记录并返回最新版本号
    query := `
        UPDATE movies
        set title = $1, year = $2, runtime = $3, genres = $4, version = version + 1
        WHERE id = $5
        RETURNING version`
    //创建args切片包含所有占位符参数值
    args := []interface{}{
        movie.Title,
        movie.Year,
        movie.Runtime,
        pq.Array(movie.Genres),
        movie.ID,
    }
    //使用QueryRow()方法执行,并以可变参数传入args切片,读取最新version值到movie结构体
    return m.DB.QueryRow(query, args...).Scan(&movie.Version)
}

需要强调的是:就像我们的Insert()方法一样Update()方法接受一个指向Movie结构体的指针作为参数,并再次原地修改它——这一次只使用新版本号更新。

创建API处理程序(handler)

现在,我们在cmd/api/movies.go文件中,添加updateMovieHandler方法。

Method URL Pattern Handler 操作
PUT /v1/movies/:id updateMovieHandler 更新特定movie信息

这个处理程序的好处在于,我们已经为它打好了所有的基础。这里的工作主要是将代码和已经编写的帮助函数串起来即可处理请求。

具体来说,我们需要:

1、 使用app.readIDParam()帮助函数从URL中提取电影ID。

2、使用我们在上一篇文章创建的Get()方法从数据库中获取相应的movie记录。

3、将包含更新movie数据的JSON请求体读入一个input结构。

4、将数据从input结构体复制到movie记录。

5、使用data.ValidateMovie()函数检查更新的movie记录各个字段是否有效。

6、调用Update()方法将新的movie信息存储到数据库中。

7、使用app.writeJSON()帮助函数将更新的movie数据写入JSON响应中。

下面开始写代码:
File: cmd/api/movies.go


package main

...

func (app *application) updateMovieHandler(w http.ResponseWriter, r *http.Request) {
    //从URL中读取要更新的movie ID
    id, err := app.readIDParam(r)
    if err != nil {
        app.notFoundResponse(w, r)
        return
    }
    //根据ID从数据库中读取旧movie信息,如果不存在就返回404 Not Found
    movie, err := app.models.Movies.Get(id)
    if err != nil {
        switch {
        case errors.Is(err, data.ErrRecordNotFound):
            app.notFoundResponse(w, r)
        default:
            app.serverErrorResponse(w, r, err)
        }
        return
    }
    //声明input结构体存放客户端发送来的数据
    var input struct {
        Title   string       `json:"title"`
        Year    int32        `json:"year"`
        Runtime data.Runtime `json:"runtime"`
        Genres  []string     `json:"genres"`
    }
    //读取JSON请求体到input结构体中
    err = app.readJSON(w, r, &input)
    if err != nil {
        app.badRequestResponse(w, r, err)
        return
    }
    //从请求体中将值拷贝到数据库movie记录对应字段
    movie.Title = input.Title
    movie.Year = input.Year
    movie.Runtime = input.Runtime
    movie.Genres = input.Genres
    //校验更新后的movie字段,如果校验失败返回422 Unprocessable Entity响应给客户端
    v := validator.New()
    if data.ValidateMovie(v, movie); !v.Valid() {
        app.failedValidationResponse(w, r, v.Errors)
        return
    }
    //将检验后的movie传给Update()方法
    err = app.models.Movies.Update(movie)
    if err != nil {
        app.serverErrorResponse(w, r, err)
        return
    }
    //将更新后到movie返回给客户端
    err = app.writeJSON(w, http.StatusOK, envelope{"movie": movie}, nil)
    if err != nil {
        app.serverErrorResponse(w, r, err)
    }
}

最后,为了完成这个任务,我们还需要更新应用程序路由以包含更新movie的API。像这样:
File:cmd/api/routers.go


package main

...

func (app *application) routes() http.Handler {
    router := httprouter.New()

    router.NotFound = http.HandlerFunc(app.notFoundResponse)
    router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse)

    router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthcheckHandler)
    router.HandlerFunc(http.MethodPost, "/v1/movies", app.createMovieHandler)
    router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.showMovieHandler)
    //为"/v1/movies/:id"接口添加路由
    router.HandlerFunc(http.MethodPut, "/v1/movies/:id", app.updateMovieHandler)
    return app.recoverPanic(app.rateLimit(router))
}

测试更新接口

现在,我们可以试试更新movie接口。

为了演示,让我们继续之前给出的例子,并更新我们的记录,使《黑豹》包含科幻题材。提醒一下,目前的记录是这样的:

$  curl -i localhost:4000/v1/movies/2 
HTTP/1.1 200 OK
Content-Type: application/json
Date: Sun, 28 Nov 2021 13:48:43 GMT
Content-Length: 145

{
        "movie": {
                "id": 2,
                "title": "Black Panther",
                "runtime": "134 mins",
                "genres": [
                        "action",
                        "adventure"
                ],
                "Version": 1
        }
}

为了更新genres字段,我们可以执行以下API调用:

$ BODY='{"title":"Black Panther","year":2018,"runtime":"134 mins","genres":["sci-fi","action","adventure"]}'
$ curl -X PUT -d "$BODY" localhost:4000/v1/movies/2
{
        "movie": {
                "id": 2,
                "title": "Black Panther",
                "runtime": "134 mins",
                "genres": [
                        "sci-fi",
                        "action",
                        "adventure"
                ],
                "Version": 2
        }
}

这看起来很棒,我们可以从响应中看到genres已经更新并包含“sci-fi”,版本号已经像我们预期的那样增加到2。

你也能够通过GET /v1/movies/2请求来验证更改是否被持久化,如下所示:

curl -i localhost:4000/v1/movies/2                                                                      
HTTP/1.1 200 OK
Content-Type: application/json
Date: Sun, 28 Nov 2021 13:52:52 GMT
Content-Length: 158

{
        "movie": {
                "id": 2,
                "title": "Black Panther",
                "runtime": "134 mins",
                "genres": [
                        "sci-fi",
                        "action",
                        "adventure"
                ],
                "Version": 2
        }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容