Let's Go Further عملیات CRUD › به‌روزرسانی یک فیلم
قبلی · فهرست مطالب · بعدی
فصل ۷.۴.

به‌روزرسانی یک فیلم

در این فصل، ساخت برنامه خود را ادامه می‌دهیم و یک نقطه اتصال جدید اضافه می‌کنیم که به مشتریان اجازه می‌دهد داده‌های یک فیلم خاص را به‌روزرسانی کنند.

عملیات هندلر الگوی URL متد
نمایش اطلاعات برنامه healthcheckHandler /v1/healthcheck GET
ایجاد یک فیلم جدید createMovieHandler /v1/movies POST
نمایش جزئیات یک فیلم خاص showMovieHandler /v1/movies/:id GET
به‌روزرسانی جزئیات یک فیلم خاص updateMovieHandler /v1/movies/:id PUT

به طور دقیق‌تر، این نقطه اتصال را طوری پیکربندی می‌کنیم که مشتری بتواند مقادیر title، year، runtime و genres یک فیلم را ویرایش کند. در پروژه ما مقادیر id و created_at پس از ایجاد نباید تغییر کنند و مقدار version چیزی نیست که مشتری باید آن را کنترل کند، بنابراین اجازه ویرایش این فیلدها را نخواهیم داد.

فعلاً، این نقطه اتصال را طوری پیکربندی می‌کنیم که یک جایگزینی کامل مقادیر یک فیلم را انجام دهد. این بدان معناست که مشتری باید مقادیر تمام فیلدهای قابل ویرایش را در بدنه درخواست JSON خود ارائه دهد... حتی اگر فقط بخواهد یکی از آنها را تغییر دهد.

برای مثال، اگر مشتری بخواهد ژانر sci-fi را به فیلم Black Panther در پایگاه داده ما اضافه کند، باید بدنه درخواست JSON زیر را ارسال کند:

{
    "title": "Black Panther",
    "year": 2018,
    "runtime": "134 mins",
    "genres": [
            "action",
            "adventure",
            "sci-fi"
    ]
}

اجرای پرس‌وجوی SQL

بیایید دوباره از مدل پایگاه داده خود شروع کنیم و متد Update() را ویرایش کنیم تا پرس‌وجوی SQL زیر را اجرا کند:

UPDATE movies 
SET title = $1, year = $2, runtime = $3, genres = $4, version = version + 1
WHERE id = $5
RETURNING version

توجه کنید که در اینجا مقدار version را به عنوان بخشی از پرس‌وجو افزایش می‌دهیم. و سپس در انتها از کلاوز RETURNING استفاده می‌کنیم تا مقدار جدید و افزایش یافته version را برگردانیم.

مانند قبل، این پرس‌وجو یک ردیف داده برمی‌گرداند، بنابراین باید از متد QueryRow() Go برای اجرای آن استفاده کنیم. اگر می‌خواهید همراه ما باشید، به فایل internal/data/movies.go خود بازگردید و متد Update() را به صورت زیر پر کنید:

فایل: internal/data/movies.go
package data

...

func (m MovieModel) Update(movie *Movie) error {
    // Declare the SQL query for updating the record and returning the new version
    // number.
    query := `
        UPDATE movies 
        SET title = $1, year = $2, runtime = $3, genres = $4, version = version + 1
        WHERE id = $5
        RETURNING version`

    // Create an args slice containing the values for the placeholder parameters.
    args := []any{
        movie.Title,
        movie.Year,
        movie.Runtime,
        pq.Array(movie.Genres),
        movie.ID,
    }

    // 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)
}

...

مهم است که تأکید کنیم — درست مانند متد Insert() ما — متد Update() یک اشاره‌گر به ساختار Movie را به عنوان پارامتر ورودی دریافت می‌کند و آن را دوباره در جا تغییر می‌دهد — این بار فقط با شماره نسخه جدید به‌روزرسانی می‌کند.

ایجاد هندلر API

اکنون بیایید به فایل cmd/api/movies.go خود بازگردیم و آن را برای شامل کردن متد جدید updateMovieHandler به‌روزرسانی کنیم.

عملیات هندلر الگوی URL متد
به‌روزرسانی جزئیات یک فیلم خاص updateMovieHandler /v1/movies/:id PUT

نکته خوب این هندلر این است که زیرساخت از قبل آماده است — کار ما در اینجا واقعاً فقط شامل پیوند دادن کد و توابع کمکی است که قبلاً برای پردازش درخواست نوشته‌ایم.

به طور خاص، باید:

  1. شناسه فیلم را از URL با استفاده از تابع کمکی app.readIDParam() استخراج کنیم.
  2. رکورد فیلم مربوطه را از پایگاه داده با استفاده از متد Get() که در فصل قبلی ساختیم، دریافت کنیم.
  3. بدنه درخواست JSON حاوی داده‌های به‌روز شده فیلم را در یک ساختار input بخوانیم.
  4. داده‌ها را از ساختار input به رکورد فیلم کپی کنیم.
  5. بررسی کنیم که رکورد به‌روز شده فیلم با استفاده از تابع data.ValidateMovie() معتبر است.
  6. متد Update() را فراخوانی کنیم تا رکورد به‌روز شده فیلم را در پایگاه داده ذخیره کند.
  7. داده‌های به‌روز شده فیلم را در یک پاسخ JSON با استفاده از تابع کمکی app.writeJSON() بنویسیم.

پس بیایید دقیقاً همین کار را انجام دهیم:

فایل: cmd/api/movies.go
package main

...

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.
    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
    }

    // Declare an input struct to hold the expected data from the client.
    var input struct {
        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.
    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

    // 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() {
        app.failedValidationResponse(w, r, v.Errors)
        return
    }

    // Pass the updated movie record to our new Update() method.
    err = app.models.Movies.Update(movie)
    if err != nil {
        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)
    }
}

در نهایت، برای تکمیل این کار، باید مسیرهای برنامه خود را نیز به‌روزرسانی کنیم تا نقطه اتصال جدید را شامل شود. به صورت زیر:

فایل: cmd/api/routes.go
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)
    // Add the route for the PUT /v1/movies/:id endpoint.
    router.HandlerFunc(http.MethodPut, "/v1/movies/:id", app.updateMovieHandler)

    return app.recoverPanic(router)
}

استفاده از نقطه اتصال جدید

و با این کار، اکنون آماده‌ایم این را آزمایش کنیم!

برای نمایش، بیایید با مثالی که در ابتدای این فصل ارائه دادیم ادامه دهیم و رکورد Black Panther خود را برای شامل کردن ژانر sci-fi به‌روزرسانی کنیم. برای یادآوری، رکورد در حال حاضر به این صورت است:

$ curl localhost:4000/v1/movies/2
{
    "movie": {
        "id": 2,
        "title": "Black Panther",
        "year": 2018,
        "runtime": "134 mins",
        "genres": [
            "action",
            "adventure"
        ],
        "version": 1
    }
}

برای اعمال به‌روزرسانی در فیلد ژانرها می‌توانیم فراخوانی API زیر را اجرا کنیم:

$ BODY='{"title":"Black Panther","year":2018,"runtime":"134 mins","genres":["sci-fi","action","adventure"]}'
$ curl -X PUT -d "$BODY" localhost:4000/v1/movies/2
{
    "movie": {
        "id": 2,
        "title": "Black Panther",
        "year": 2018,
        "runtime": "134 mins",
        "genres": [
            "sci-fi",
            "action",
            "adventure"
        ],
        "version": 2
    }
}

عالی به نظر می‌رسد — از پاسخ می‌توانیم ببینیم که ژانرهای فیلم برای شامل کردن "sci-fi" به‌روزرسانی شده‌اند و شماره نسخه مطابق انتظار به 2 افزایش یافته است.

همچنین می‌توانید با ارسال درخواست GET /v1/movies/2 دوباره بررسی کنید که تغییر ذخیره شده است، به صورت زیر:

$ curl localhost:4000/v1/movies/2
{
    "movie": {
        "id": 2,
        "title": "Black Panther",
        "year": 2018,
        "runtime": "134 mins",
        "genres": [
            "sci-fi",
            "action",
            "adventure"
        ],
        "version": 2
    }
}