Let's Go Further CRUD operations › Fetching a movie
Previous · Contents · Next
Chapter 7.3.

Fetching a movie

Now let’s move on to the code for fetching and displaying the data for a specific movie.

Again, we’ll begin in our database model here, and start by updating the Get() method to execute the following SQL query:

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

Since our movies table uses the id column as its primary key, this query will only ever return exactly one database row (or none at all). So, it’s appropriate for us to execute this query using Go’s QueryRow() method again.

If you’re following along, open up your internal/data/movies.go file and update it like so:

File: 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
}

...

Hopefully the code above should feel clear and familiar — it’s a straight lift of the pattern that we discussed in detail in Let’s Go.

The only real thing of note is the fact that we need to use the pq.Array() adapter again when scanning in the genres data from the PostgreSQL text[] array. If we didn’t use this adapter, we would get the following error at runtime:

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

Updating the API handler

OK, the next thing we need to do is update our showMovieHandler so that it calls the Get() method we just made. The handler should check whether Get() returns an ErrRecordNotFound error — and if it does, the client should be sent a 404 Not Found response. Otherwise, we can go ahead and render the returned Movie struct in a JSON response.

Like so:

File: 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)
    }
}

Great! That’s all nice and succinct, thanks to the structure and helpers that we’ve already put in place.

Feel free to give this a try by restarting the API and looking up a movie that you’ve already created in the database. For example:

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

And likewise, you can also try making a request with a movie ID that doesn’t exist in the database yet (but is otherwise valid). In that scenario you should receive a 404 Not Found response like so:

$ 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"
}

Additional information

Why not use an unsigned integer for the movie ID?

At the start of the Get() method we have the following code which checks if the movie id parameter is less than 1:

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

    ...
}

This might have led you to wonder: if the movie ID is never negative, why aren’t we using an unsigned uint64 type to store the ID in our Go code, instead of an int64?

There are two reasons for this.

The first reason is because PostgreSQL doesn’t have unsigned integers. It’s generally sensible to align your Go and database integer types to avoid overflows or other compatibility problems, so because PostgreSQL doesn’t have unsigned integers, this means that we should avoid using uint* types in our Go code for any values that we’re reading/writing to PostgreSQL too.

Instead, it’s best to align the integer types based on the following table:

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

There’s also another, more subtle, reason. Go’s database/sql package doesn’t actually support any integer values greater than 9223372036854775807 (the maximum value for an int64). It’s possible that a uint64 value could be greater than this, which would in turn lead to Go generating a runtime error similar to this:

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

By sticking with an int64 in our Go code, we eliminate the risk of ever encountering this error.