feat: update changes, race condition check on version, put -> patch

This commit is contained in:
2026-03-17 16:26:33 +01:00
parent bcf16bc4c6
commit 973a9e341d
10 changed files with 67 additions and 25 deletions
Vendored
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+5
View File
@@ -63,3 +63,8 @@ func (app *application) methodNotAllowedResponse(w http.ResponseWriter, r *http.
func (app *application) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) {
app.errorResponse(w, r, http.StatusBadRequest, err.Error())
}
func (app *application) editConflictResponse(w http.ResponseWriter, r *http.Request) {
message := "unable to update the record due to an edit conflict, please try again"
app.errorResponse(w, r, http.StatusConflict, message)
}
+43 -18
View File
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/http"
"strconv"
"greenlight.debuggingjon.dev/internal/data"
"greenlight.debuggingjon.dev/internal/validator"
@@ -93,15 +94,13 @@ func (app *application) showMovieHandler(w http.ResponseWriter, r *http.Request)
}
func (app *application) updateMovieHandler(w http.ResponseWriter, r *http.Request) {
// Extract the movie ID from the URL.
id, err := app.readIDParam(r)
if err != nil {
app.notFoundResponse(w, r)
return
}
// Fetch the existing movie record from the database, sending a 404 Not Found
// response to the client if we couldn't find a matching record.
// Retrieve the movie record as normal.
movie, err := app.models.Movies.Get(id)
if err != nil {
switch {
@@ -112,30 +111,51 @@ func (app *application) updateMovieHandler(w http.ResponseWriter, r *http.Reques
}
return
}
// Declare an input struct to hold the expected data from the client.
// If the request contains a X-Expected-Version header, verify that the movie
// version in the database matches the expected version specified in the header.
if r.Header.Get("X-Expected-Version") != "" {
if strconv.FormatInt(int64(movie.Version), 32) != r.Header.Get("X-Expected-Version") {
app.editConflictResponse(w, r)
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"`
Title *string `json:"title"`
Year *int32 `json:"year"`
Runtime *data.Runtime `json:"runtime"`
Genres []string `json:"genres"`
}
// Read the JSON request body data into the input struct.
// Decode the JSON as normal.
err = app.readJSON(w, r, &input)
if err != nil {
app.badRequestResponse(w, r, err)
return
}
// Copy the values from the request body to the appropriate fields of the movie
// record.
movie.Title = input.Title
movie.Year = input.Year
movie.Runtime = input.Runtime
movie.Genres = input.Genres
// 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 a 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.
}
// Validate the updated movie record, sending the client a 422 Unprocessable Entity
// response if any checks fail.
v := validator.New()
if data.ValidateMovie(v, movie); !v.Valid() {
@@ -143,14 +163,19 @@ func (app *application) updateMovieHandler(w http.ResponseWriter, r *http.Reques
return
}
// Pass the updated movie record to our new Update() method.
// Intercept any ErrEditConflict error and call the new editConflictResponse()
// helper.
err = app.models.Movies.Update(movie)
if err != nil {
switch {
case errors.Is(err, data.ErrEditConflict):
app.editConflictResponse(w, r)
default:
app.serverErrorResponse(w, r, err)
}
return
}
// Write the updated movie record in a JSON response.
err = app.writeJSON(w, http.StatusOK, envelope{"movie": movie}, nil)
if err != nil {
app.serverErrorResponse(w, r, err)
+1 -1
View File
@@ -21,7 +21,7 @@ func (app *application) routes() *httprouter.Router {
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)
router.HandlerFunc(http.MethodPut, "/v1/movies/:id", app.updateMovieHandler)
router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.updateMovieHandler)
router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.deleteMovieHandler)
return router
Binary file not shown.
@@ -5,4 +5,4 @@ variables:
- name: VERSION
value: v1
- name: MOVIE_ID
value: "8"
value: "1"
@@ -9,6 +9,7 @@ import (
// looking up a movie that doesn't exist in our database.
var (
ErrRecordNotFound = errors.New("record not found")
ErrEditConflict = errors.New("edit conflict")
)
// Create a Models struct which wraps the MovieModel. We'll add other models to this,
+13 -2
View File
@@ -86,7 +86,7 @@ func (m MovieModel) Update(movie *Movie) error {
query := `
UPDATE movies
SET title = $1, year = $2, runtime = $3, genres = $4, version = version + 1
WHERE id = $5
WHERE id = $5 AND version = $6
RETURNING version`
// Create an args slice containing the values for the placeholder parameters.
@@ -96,11 +96,22 @@ func (m MovieModel) Update(movie *Movie) error {
movie.Runtime,
pq.Array(movie.Genres),
movie.ID,
movie.Version,
}
// Use the QueryRow() method to execute the query, passing in the args slice as a
// variadic parameter and scanning the new version value into the movie struct.
return m.DB.QueryRow(query, args...).Scan(&movie.Version)
err := m.DB.QueryRow(query, args...).Scan(&movie.Version)
if err != nil {
switch {
case errors.Is(err, sql.ErrNoRows):
return ErrEditConflict
default:
return err
}
}
return nil
}
func (m MovieModel) Delete(id int64) error {