go

【Go Web开发】PATCH请求处理

2022-02-11  本文已影响0人  Go语言由浅入深

前面几篇文章我们介绍了Go Web的增删改查(CRUD)请求处理,接下来我们将看一些更高级的CRUD操作。你将学习到以下内容:

部分更新处理

本节,我们将更改updateMovieHandler的行为,以便支持对数据库中movie记录的部分更新。从概念上讲,这比完全替换要复杂一些,这就是为什么我们首先用这种方法来打基础。

假设在数据库中电影theBreakfastClub的发布年份是错误的(实际上应该是1985年,而不是1986年)。如果我们可以发送一个只包含需要修改字段的JSON请求,而不是movie所有的数据效果会更好,就像下面这样:

 {"year": 1985}

让我们快速地看看如果我们现在发送这个请求会发生什么:

$ curl -X PUT -d '{"year": 1985}' localhost:4000/v1/movies/4
{
        "error": {
                "genres": "must contain at least 1 genres",
                "runtime": "must be provided",
                "title": "must be provide"
        }
}

正如我们在前面提到的,当反序列化请求体时,我们的input结构体中任何没有对应的JSON键/值对的字段,将保留它们的零值。碰巧在字段校验时会检查这些零值,并返回上面看到的错误消息。

在部分更新的场景中,就出现以下问题。我们如何区分:

为了回答这个问题,让我们快速地看下不同Go类型的零值是什么。

Go类型 零值
int, unit, float, complex 0
string ""
bool false
func, array, slice, map, chan, pointers nil

这里需要注意的是指针的零值是nil。

因此,理论上,我们可以将input结构体中的字段更改为指针类型。然后,要查看客户端是否在JSON中提供了特定的键/值对,我们可以简单地检查输入结构中的对应字段是否等于nil。

// 定义Title, Year和Runtime字段为指针类型.
var input struct {
    Title *string         `json:"title"` // 反序列化时如果JSON中没有对应的键,则该值为nil。
    Year *int32           `json:"year"` // Likewise...
    Runtime *data.Runtime `json:"runtime"` // Likewise...
    Genres []string       `json:"genres"` // 我们不需要改变这个,因为切片已经有了0值nil。
}

执行部分更新

下面进入项目实践阶段,编辑updateMovieHandler方法,使其支持部分更新:

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记录对应字段
    //根据指针是否为nil,判断客户端是否传了值,排除默认值干扰
    if input.Title != nil {
        movie.Title = *input.Title
    }
    if input.Year != nil {
        movie.Year = *input.Year
    }
    if input.Runtime != nil {
        movie.Runtime = *input.Runtime
    }
    if input.Genres != nil {
        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)
    }
}

总结一下:我们已经改变了input结构体,所有的字段零值都是nil。解析JSON请求后,遍历input结构各个字段,只在新值不为nil的情况下更新数据库movie记录。

除此之外,对于在资源上执行部分更新的API接口,使用HTTP的PATCH方法而不是PUT(PUT是完全替换一个资源)也是可以的。

因此,在尝试新代码之前,让我们快速更新下我们的cmd/api/routes.go文件,让updateMovieHandler只用于PATCH请求。

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)
    //使用PATCH请求,而不是PUT
    router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.updateMovieHandler)
    router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.deleteMovieHandler)
    return app.recoverPanic(app.rateLimit(router))
}

演示效果

完成上面的代码更新后,通过将数据库中存储电影The Breakfast Club的发布年份修正到1985,来检查部分更新功能是否有效。像这样:

$ curl -X PATCH  -d '{"year": 1985}' localhost:4000/v1/movies/4
{
        "movie": {
                "id": 4,
                "title": "The Breakfast Club",
                "year": 1985,
                "runtime": "96 mins",
                "genres": [
                        "drama"
                ],
                "Version": 2
        }
}

看起来不错。我们可以看到year值已经正确更新,版本号已经增加,但是其他数据字段都没有更改。

我们尝试相同的请求,但是包含一个空的title值。在这种情况下,更新将被阻止,你应该收到一个验证错误,像这样:

$ curl -X PATCH -d '{"year": 1985, "title": ""}' localhost:4000/v1/movies/4
{
        "error": {
                "title": "must be provide"
        }
}

附加内容

JSON中的NULL值

有一种特殊情况,客户端如果显式地在请求中,给JSON字段赋值为null的话。这种情况,我们的处理程序反序列化后指针类型值也是nil。也就是相当于上面客户端未提供该字段的效果。

例如,以下请求将不会触发对电影记录的更改(除了版本号会增加):

 curl -X PATCH -d '{"title": null, "year": null}' localhost:4000/v1/movies/4
{
        "movie": {
                "id": 4,
                "title": "The Breakfast Club",
                "year": 1985,
                "runtime": "96 mins",
                "genres": [
                        "drama"
                ],
                "Version": 3
        }
}

在理想情况下,这种类型的请求将返回某种验证错误。除非您编写自定义JSON解析器,否则无法确定客户端在JSON中不提供键/值对与提供值null之间的区别。

在大多数情况下,在接口文档中会解释这种特殊情况,并说明“带有空值的JSON字段将被忽略并保持不变”就足够了。

上一篇 下一篇

猜你喜欢

热点阅读