مدیریت بهروزرسانیهای جزئی
در این فصل قصد داریم رفتار updateMovieHandler را تغییر دهیم تا از بهروزرسانیهای جزئی رکوردهای فیلم پشتیبانی کند. از نظر مفهومی این کار کمی پیچیدهتر از جایگزینی کامل است، به همین دلیل ابتدا پایه و اساس کار را با آن روش فراهم کردیم.
به عنوان مثال، فرض کنید متوجه شدهایم که سال اکران The Breakfast Club در پایگاه داده ما نادرست است (در واقع باید ۱۹۸۵ باشد، نه ۱۹۸۶). خوب بود اگر میتوانستیم یک درخواست JSON ارسال کنیم که فقط شامل تغییری باشد که باید اعمال شود، به جای کل اطلاعات فیلم، به این صورت:
{"year": 1985}
بیایید سریعاً بررسی کنیم که اگر الان این درخواست را ارسال کنیم چه اتفاقی میافتد:
$ curl -X PUT -d '{"year": 1985}' localhost:4000/v1/movies/4
{
"error": {
"genres": "must be provided",
"runtime": "must be provided",
"title": "must be provided"
}
}
همانطور که قبلاً در کتاب اشاره کردیم، هنگام رمزگشایی بدنه درخواست، هر فیلدی در ساختار input ما که جفت کلید-مقدار متناظری در JSON نداشته باشد، مقدار صفر خود را حفظ میکند. ما در مرحله اعتبارسنجی این مقادیر صفر را بررسی میکنیم و پیامهای خطایی که در بالا میبینید را برمیگردانیم.
در زمینه بهروزرسانی جزئی، این موضوع مشکل ایجاد میکند. چگونه تفاوت بین این موارد را تشخیص دهیم:
- یک کلاینت جفت کلید-مقداری با مقدار صفر ارائه میدهد — مانند
{"title": ""}— که در این صورت میخواهیم خطای اعتبارسنجی برگردانیم. - یک کلاینت اصلاً جفت کلید-مقداری ارائه نمیدهد در JSON خود — که در این صورت میخواهیم بهروزرسانی فیلد را ‘رد’ کنیم اما خطای اعتبارسنجی ارسال نکنیم.
برای کمک به پاسخ به این سؤال، بیایید سریعاً مقادیر صفر برای انواع مختلف Go را مرور کنیم.
| نوع Go | مقدار صفر |
|---|---|
int*, uint*, float*, complex |
0 |
string |
"" |
bool |
false |
func, array, slice, map, chan و اشارهگرها |
nil |
نکته کلیدی که باید توجه کنید این است که اشارهگرها مقدار صفر nil دارند.
بنابراین — از نظر تئوری — میتوانیم فیلدهای ساختار input خود را به اشارهگر تغییر دهیم. سپس برای بررسی اینکه آیا کلاینت جفت کلید-مقدار خاصی در JSON ارائه داده است یا خیر، میتوانیم به سادگی بررسی کنیم که آیا فیلد متناظر در ساختار input برابر با nil است یا خیر.
// Use pointers for the Title, Year and Runtime fields. var input struct { Title *string `json:"title"` // This will be nil if there is no corresponding key in the JSON. Year *int32 `json:"year"` // Likewise... Runtime *data.Runtime `json:"runtime"` // Likewise... Genres []string `json:"genres"` // We don't need to change this because slices already have the zero value nil. }
انجام بهروزرسانی جزئی
بیایید این را عملی کنیم و متد updateMovieHandler خود را ویرایش کنیم تا از بهروزرسانیهای جزئی به صورت زیر پشتیبانی کند:
package main ... func (app *application) updateMovieHandler(w http.ResponseWriter, r *http.Request) { id, err := app.readIDParam(r) if err != nil { app.notFoundResponse(w, r) return } // Retrieve the movie record as normal. 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 } // Use pointers for the Title, Year and Runtime fields. var input struct { Title *string `json:"title"` Year *int32 `json:"year"` Runtime *data.Runtime `json:"runtime"` Genres []string `json:"genres"` } // Decode the JSON as normal. err = app.readJSON(w, r, &input) if err != nil { app.badRequestResponse(w, r, err) return } // If the input.Title value is nil, then we know that no corresponding "title" // key-value pair was provided in the JSON request body. So we move on and leave the // movie record unchanged. Otherwise, we update the movie record with the new title // value. Importantly, because input.Title is now a pointer to a string, we need // to dereference the pointer using the * operator to get the underlying value // before assigning it to our movie record. if input.Title != nil { movie.Title = *input.Title } // We also do the same for the other fields in the input struct. if input.Year != nil { movie.Year = *input.Year } if input.Runtime != nil { movie.Runtime = *input.Runtime } if input.Genres != nil { movie.Genres = input.Genres // Note that we don't need to dereference a slice. } v := validator.New() if data.ValidateMovie(v, movie); !v.Valid() { app.failedValidationResponse(w, r, v.Errors) return } err = app.models.Movies.Update(movie) if err != nil { app.serverErrorResponse(w, r, err) return } err = app.writeJSON(w, http.StatusOK, envelope{"movie": movie}, nil) if err != nil { app.serverErrorResponse(w, r, err) } }
خلاصه این تغییرات این است: ما ساختار input خود را طوری تغییر دادیم که تمام فیلدها حالا مقدار صفر nil دارند. پس از پردازش درخواست JSON، فیلدهای ساختار input را بررسی میکنیم و فقط در صورتی که مقدار جدید نباشد nil، رکورد فیلم را بهروزرسانی میکنیم.
علاوه بر این، برای endpointهای API که بهروزرسانیهای جزئی روی یک منبع انجام میدهند، مناسب است از متد HTTP PATCH به جای PUT استفاده شود (که برای جایگزینی کامل یک منبع در نظر گرفته شده است).
بنابراین، قبل از اینکه کد جدید خود را آزمایش کنیم، بیایید سریعاً فایل cmd/api/routes.go خود را بهروزرسانی کنیم تا updateMovieHandler ما فقط برای درخواستهای PATCH استفاده شود.
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) // Require a PATCH request, rather than PUT. router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.updateMovieHandler) router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.deleteMovieHandler) return app.recoverPanic(router) }
نمایش عملی
با راهاندازی کامل این موارد، بیایید بررسی کنیم که عملکرد بهروزرسانی جزئی با اصلاح سال اکران The Breakfast Club به ۱۹۸۵ کار میکند. به این صورت:
$ 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 به درستی بهروزرسانی شده و شماره version افزایش یافته است، اما هیچیک از فیلدهای داده دیگر تغییر نکردهاند.
بیایید همچنین سریعاً همان درخواست را با شامل کردن یک مقدار خالی title امتحان کنیم. در این صورت بهروزرسانی مسدود میشود و باید خطای اعتبارسنجی دریافت کنید، به این صورت:
$ curl -X PATCH -d '{"year": 1985, "title": ""}' localhost:4000/v1/movies/4
{
"error": {
"title": "must be provided"
}
}
اطلاعات تکمیلی
مقادیر null در JSON
یک حالت خاص که باید از آن آگاه باشید زمانی است که کلاینت به صورت صریح فیلدی در درخواست JSON با مقدار null ارائه میدهد. در این صورت، هندلر ما آن فیلد را نادیده میگیرد و آن را مانند زمانی که ارائه نشده رفتار میکند.
به عنوان مثال، درخواست زیر هیچ تغییری در رکورد فیلم ایجاد نمیکند (به جز افزایش شماره version):
$ 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 وجود ندارد.
در بیشتر موارد، احتمالاً توضیح این رفتار حالت خاص در مستندات کلاینت برای endpoint کافی است و چیزی شبیه به “آیتمهای JSON با مقادیر null نادیده گرفته میشوند و بدون تغییر باقی میمانند” گفته شود.