美文网首页go
【Go Web开发】PATCH请求处理

【Go Web开发】PATCH请求处理

作者: Go语言由浅入深 | 来源:发表于2022-02-11 23:39 被阅读0次

    前面几篇文章我们介绍了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键/值对的字段,将保留它们的零值。碰巧在字段校验时会检查这些零值,并返回上面看到的错误消息。

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

    • 如果客户端提供的key/value就是包含字段零值,例如“{title:""}”,这时我们需要返回校验失败。
    • 如果客户端在请求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字段将被忽略并保持不变”就足够了。

    相关文章

      网友评论

        本文标题:【Go Web开发】PATCH请求处理

        本文链接:https://www.haomeiwen.com/subject/hanzkrtx.html