前面几篇文章我们介绍了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字段将被忽略并保持不变”就足够了。
网友评论