Setting up the movie model
In this chapter we’re going to set up the skeleton code for our movie database model.
If you don’t like the term model then you might want to think of this as your data access or storage layer instead. But whatever you prefer to call it, the principle is the same — it will encapsulate all the code for reading and writing movie data to and from our PostgreSQL database.
Let’s head back to the internal/data/movies.go file and create a MovieModel struct type and some placeholder methods for performing basic CRUD (create, read, update and delete) actions against our movies database table.
package data import ( "database/sql" // New import "time" "greenlight.alexedwards.net/internal/validator" ) ... // Define a MovieModel struct type which wraps a sql.DB connection pool. type MovieModel struct { DB *sql.DB } // Add a placeholder method for inserting a new record in the movies table. func (m MovieModel) Insert(movie *Movie) error { return nil } // Add a placeholder method for fetching a specific record from the movies table. func (m MovieModel) Get(id int64) (*Movie, error) { return nil, nil } // Add a placeholder method for updating a specific record in the movies table. func (m MovieModel) Update(movie *Movie) error { return nil } // Add a placeholder method for deleting a specific record from the movies table. func (m MovieModel) Delete(id int64) error { return nil }
As an additional step, we’re going to wrap our MovieModel in a parent Models struct. Doing this is totally optional, but it has the benefit of giving you a convenient single ‘container’ which can hold and represent all your database models as your application grows.
If you’re following along, create a new internal/data/models.go file and add the following code:
$ touch internal/data/models.go
package data import ( "database/sql" "errors" ) // Define a custom ErrRecordNotFound error. We'll return this from our Get() method when // looking up a movie that doesn't exist in our database. var ( ErrRecordNotFound = errors.New("record not found") ) // Create a Models struct which wraps the MovieModel. We'll add other models to this, // like a UserModel and PermissionModel, as our build progresses. type Models struct { Movies MovieModel } // For ease of use, we also add a New() method which returns a Models struct containing // the initialized MovieModel. func NewModels(db *sql.DB) Models { return Models{ Movies: MovieModel{DB: db}, } }
And now, let’s edit our cmd/api/main.go file so that the Models struct is initialized in our main() function, and then passed to our handlers as a dependency. Like so:
package main import ( "context" "database/sql" "flag" "fmt" "log/slog" "net/http" "os" "time" "greenlight.alexedwards.net/internal/data" // New import _ "github.com/lib/pq" ) ... // Add a models field to hold our new Models struct. type application struct { config config logger *slog.Logger models data.Models } func main() { var cfg config flag.IntVar(&cfg.port, "port", 4000, "API server port") flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)") flag.StringVar(&cfg.db.dsn, "db-dsn", os.Getenv("GREENLIGHT_DB_DSN"), "PostgreSQL DSN") flag.IntVar(&cfg.db.maxOpenConns, "db-max-open-conns", 25, "PostgreSQL max open connections") flag.IntVar(&cfg.db.maxIdleConns, "db-max-idle-conns", 25, "PostgreSQL max idle connections") flag.DurationVar(&cfg.db.maxIdleTime, "db-max-idle-time", 15*time.Minute, "PostgreSQL max connection idle time") flag.Parse() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) db, err := openDB(cfg) if err != nil { logger.Error(err.Error()) os.Exit(1) } defer db.Close() logger.Info("database connection pool established") // Use the data.NewModels() function to initialize a Models struct, passing in the // connection pool as a parameter. app := &application{ config: cfg, logger: logger, models: data.NewModels(db), } srv := &http.Server{ Addr: fmt.Sprintf(":%d", cfg.port), Handler: app.routes(), IdleTimeout: time.Minute, ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError), } logger.Info("starting server", "addr", srv.Addr, "env", cfg.env) err = srv.ListenAndServe() logger.Error(err.Error()) os.Exit(1) } ...
If you want, you can try restarting the application at this point. You should find that the code compiles and runs successfully.
$ go run ./cmd/api time=2023-09-10T10:59:13.722+02:00 level=INFO msg="database connection pool established" time=2023-09-10T10:59:13.722+02:00 level=INFO msg="starting server" addr=:4000 env=development
One of the nice things about this pattern is that the code to execute actions on our movies table will be very clear and readable from the perspective of our API handlers. For example, we’ll be able to execute the Insert() method by simply writing:
app.models.Movies.Insert(...)
The general structure is also easy to extend. When we create more database models in the future, all we have to do is include them in the Models struct and they will automatically be available to our API handlers.