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

دریافت یک فیلم

حال بیایید به سراغ کد دریافت و نمایش داده‌های یک فیلم خاص برویم.

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

SELECT id, created_at, title, year, runtime, genres, version
FROM movies
WHERE id = $1

از آنجایی که جدول movies ما از ستون id به‌عنوان کلید اصلی استفاده می‌کند، این کوئری فقط دقیقاً یک ردیف از پایگاه داده را برمی‌گرداند (یا هیچ ردیفی برنمی‌گرداند). بنابراین، مناسب است که این کوئری را با استفاده از متد QueryRow() Go اجرا کنیم.

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

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

import (
    "database/sql"
    "errors" // New import
    "time"
    
    "greenlight.alexedwards.net/internal/validator"

    "github.com/lib/pq"
)

...

func (m MovieModel) Get(id int64) (*Movie, error) {
    // The PostgreSQL bigserial type that we're using for the movie ID starts
    // auto-incrementing at 1 by default, so we know that no movies will have ID values
    // less than that. To avoid making an unnecessary database call, we take a shortcut
    // and return an ErrRecordNotFound error straight away.
    if id < 1 {
        return nil, ErrRecordNotFound
    }

    // Define the SQL query for retrieving the movie data.
    query := `
        SELECT id, created_at, title, year, runtime, genres, version
        FROM movies
        WHERE id = $1`

    // Declare a Movie struct to hold the data returned by the query.
    var movie Movie

    // Execute the query using the QueryRow() method, passing in the provided id value  
    // as a placeholder parameter, and scan the response data into the fields of the 
    // Movie struct. Importantly, note that we need to convert the scan target for the 
    // genres column using the pq.Array() adapter function again.
    err := m.DB.QueryRow(query, id).Scan(
        &movie.ID,
        &movie.CreatedAt,
        &movie.Title,
        &movie.Year,
        &movie.Runtime,
        pq.Array(&movie.Genres),
        &movie.Version,
    )

    // Handle any errors. If there was no matching movie found, Scan() will return 
    // a sql.ErrNoRows error. We check for this and return our custom ErrRecordNotFound 
    // error instead. 
    if err != nil {
        switch {
        case errors.Is(err, sql.ErrNoRows):
            return nil, ErrRecordNotFound
        default:
            return nil, err
        }
    }

    // Otherwise, return a pointer to the Movie struct.
    return &movie, nil
}

...

امیدواریم کد بالا واضح و آشنا باشد — این دقیقاً همان الگویی است که در Let's Go به‌طور مفصل در مورد آن بحث کردیم.

نکته قابل توجه این است که باید از آداپتور pq.Array() دوباره هنگام اسکن داده‌های ژانر از آرایه text[] PostgreSQL استفاده کنیم. اگر از این آداپتور استفاده نکنیم، خطای زیر را در زمان اجرا دریافت خواهیم کرد:

sql: Scan error on column index 5, name "genres": unsupported Scan, storing driver.Value type []uint8 into type *[]string

به‌روزرسانی هندلر API

خب، کار بعدی که باید انجام دهیم به‌روزرسانی showMovieHandler است تا متد Get() که ایجاد کردیم را فراخوانی کند. هندلر باید بررسی کند که آیا Get() خطای ErrRecordNotFound برمی‌گرداند یا خیر — و اگر چنین است، پاسخ 404 Not Found به کلاینت ارسال شود. در غیر این صورت، می‌توانیم ساختار Movie برگشتی را در یک پاسخ JSON رندر کنیم.

به صورت زیر:

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

import (
    "errors" // New import
    "fmt"
    "net/http"

    "greenlight.alexedwards.net/internal/data"
    "greenlight.alexedwards.net/internal/validator"
)

...

func (app *application) showMovieHandler(w http.ResponseWriter, r *http.Request) {
    id, err := app.readIDParam(r)
    if err != nil {
        app.notFoundResponse(w, r)
        return
    }

    // Call the Get() method to fetch the data for a specific movie. We also need to 
    // use the errors.Is() function to check if it returns a data.ErrRecordNotFound
    // error, in which case we send a 404 Not Found response to the client.
    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
    }

    err = app.writeJSON(w, http.StatusOK, envelope{"movie": movie}, nil)
    if err != nil {
        app.serverErrorResponse(w, r, err)
    }
}

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

می‌توانید با ریستارت کردن API و جستجوی یک فیلم که قبلاً در پایگاه داده ایجاد کرده‌اید، این کد را امتحان کنید. به عنوان مثال:

$ curl -i localhost:4000/v1/movies/2
HTTP/1.1 200 OK
Content-Type: application/json
Date: Wed, 07 Apr 2021 19:37:12 GMT
Content-Length: 161

{
    "movie": {
        "id": 2,
        "title": "Black Panther",
        "year": 2018,
        "runtime": "134 mins",
        "genres": [
            "action",
            "adventure"
        ],
        "version": 1
    }
}

به همین ترتیب، می‌توانید درخواستی با شناسه فیلمی بفرستید که هنوز در پایگاه داده وجود ندارد (اما در غیر این صورت معتبر است). در آن سناریو باید پاسخ 404 Not Found مانند زیر دریافت کنید:

$ curl -i localhost:4000/v1/movies/42
HTTP/1.1 404 Not Found
Content-Type: application/json
Date: Wed, 07 Apr 2021 19:37:58 GMT
Content-Length: 58

{
    "error": "the requested resource could not be found"
}

اطلاعات تکمیلی

چرا از عدد صحیح بدون علامت برای شناسه فیلم استفاده نمی‌کنیم؟

در ابتدای متد Get() کد زیر را داریم که بررسی می‌کند آیا پارامتر id فیلم کمتر از ۱ است یا خیر:

func (m MovieModel) Get(id int64) (*Movie, error) {
    if id < 1 {
        return nil, ErrRecordNotFound
    }

    ...
}

شاید این سؤال در ذهن شما ایجاد شده باشد: اگر شناسه فیلم هرگز منفی نیست، چرا از نوع uint64 بدون علامت به جای int64 برای ذخیره شناسه در کد Go خود استفاده نمی‌کنیم؟

دو دلیل برای این وجود دارد.

دلیل اول این است که PostgreSQL عدد صحیح بدون علامت ندارد. به‌طور کلی هوشمندانه است که انواع عدد صحیح Go و پایگاه داده خود را هماهنگ کنید تا از overflow یا سایر مشکلات سازگاری جلوگیری شود، بنابراین از آنجایی که PostgreSQL عدد صحیح بدون علامت ندارد، این بدان معناست که باید از استفاده انواع uint* در کد Go خود برای هر مقداری که از/به PostgreSQL می‌خوانیم/می‌نویسیم خودداری کنیم.

بهتر است انواع عدد صحیح را بر اساس جدول زیر هماهنگ کنید:

نوع PostgreSQL نوع Go
smallint, smallserial int16 (-32768 to 32767)
integer, serial int32 (-2147483648 to 2147483647)
bigint, bigserial int64 (-9223372036854775808 to 9223372036854775807)

دلیل دیگری نیز وجود دارد که ظریف‌تر است. پکیج database/sql Go واقعاً از مقادیر عدد صحیح بزرگتر از 9223372036854775807 (حداکثر مقدار برای int64) پشتیبانی نمی‌کند. ممکن است مقدار uint64 از این بزرگتر باشد که در نتیجه باعث ایجاد خطای زمان اجرا مشابه زیر توسط Go می‌شود:

sql: converting argument $1 type: uint64 values with high bit set are not supported

با استفاده از int64 در کد Go خود، خطر بروز این خطا را از بین می‌بریم.