feat: sql, migrations, sql postgress db setup, movie model
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
_ "github.com/lib/pq"
|
||||
"greenlight.debuggingjon.dev/internal/data"
|
||||
)
|
||||
|
||||
// Declare a string containing the application version number. Later in the book we'll
|
||||
@@ -28,7 +29,10 @@ type config struct {
|
||||
port int
|
||||
env string
|
||||
db struct {
|
||||
dsn string
|
||||
dsn string
|
||||
maxOpenConns int
|
||||
maxIdleConns int
|
||||
maxIdleTime string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +42,7 @@ type config struct {
|
||||
type application struct {
|
||||
config config
|
||||
logger *log.Logger
|
||||
models data.Models
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -54,7 +59,12 @@ func main() {
|
||||
flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)")
|
||||
// Read the DSN value from the db-dsn command-line flag into the config struct. We
|
||||
// default to using our development DSN if no flag is provided.
|
||||
flag.StringVar(&cfg.db.dsn, "db-dsn", dsn, "PostgreSQL DSN (From DATABASE_DSN in .env)")
|
||||
flag.StringVar(&cfg.db.dsn, "db-dsn", dsn, "PostgreSQL DSN (Default from DATABASE_DSN in .env)")
|
||||
// Read the connection pool settings from command-line flags into the config struct.
|
||||
// Notice the default values that we're using?
|
||||
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.StringVar(&cfg.db.maxIdleTime, "db-max-idle-time", "15m", "PostgreSQL max connection idle time")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
@@ -80,6 +90,7 @@ func main() {
|
||||
app := &application{
|
||||
config: cfg,
|
||||
logger: logger,
|
||||
models: data.NewModels(db),
|
||||
}
|
||||
|
||||
// Use the httprouter instance returned by app.routes() as the server handler.
|
||||
@@ -97,26 +108,36 @@ func main() {
|
||||
}
|
||||
|
||||
func openDB(cfg config) (*sql.DB, error) {
|
||||
// Use sql.Open() to create an empty connection pool, using the DSN from the config
|
||||
// struct.
|
||||
db, err := sql.Open("postgres", cfg.db.dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create a context with a 5-second timeout deadline.
|
||||
// Set the maximum number of open (in-use + idle) connections in the pool. Note that
|
||||
// passing a value less than or equal to 0 will mean there is no limit.
|
||||
db.SetMaxOpenConns(cfg.db.maxOpenConns)
|
||||
|
||||
// Set the maximum number of idle connections in the pool. Again, passing a value
|
||||
// less than or equal to 0 will mean there is no limit.
|
||||
db.SetMaxIdleConns(cfg.db.maxIdleConns)
|
||||
|
||||
// Use the time.ParseDuration() function to convert the idle timeout duration string
|
||||
// to a time.Duration type.
|
||||
duration, err := time.ParseDuration(cfg.db.maxIdleTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set the maximum idle timeout.
|
||||
db.SetConnMaxIdleTime(duration)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Use PingContext() to establish a new connection to the database, passing in the
|
||||
// context we created above as a parameter. If the connection couldn't be
|
||||
// established successfully within the 5 second deadline, then this will return an
|
||||
// error.
|
||||
err = db.PingContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return the sql.DB connection pool.
|
||||
return db, nil
|
||||
}
|
||||
|
||||
@@ -41,7 +41,28 @@ func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%+v\n", input)
|
||||
// Call the Insert() method on our movies model, passing in a pointer to the
|
||||
// validated movie struct. This will create a record in the database and update the
|
||||
// movie struct with the system-generated information.”
|
||||
err = app.models.Movies.Insert(movie)
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// When sending a HTTP response, we want to include a Location header to let the
|
||||
// client know which URL they can find the newly-created resource at. We make an
|
||||
// empty http.Header map and then use the Set() method to add a new Location header,
|
||||
// interpolating the system-generated ID for our new movie in the URL.
|
||||
headers := make(http.Header)
|
||||
headers.Set("Location", fmt.Sprintf("/v1/movies/%d", movie.ID))
|
||||
|
||||
// Write a JSON response with a 201 Created status code, the movie data in the
|
||||
// response body, and the Location header.
|
||||
err = app.writeJSON(w, http.StatusCreated, envelope{"movie": movie}, headers)
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) showMovieHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
+4
-2
@@ -10,8 +10,10 @@ http:
|
||||
type: json
|
||||
data: |-
|
||||
{
|
||||
"title": "2313",
|
||||
"runtime": "2"
|
||||
"title": "The Breakfast Club",
|
||||
"runtime": "96 mins",
|
||||
"year": 1986,
|
||||
"genres": ["drama"]
|
||||
}
|
||||
auth: inherit
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
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},
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,53 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"greenlight.debuggingjon.dev/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 {
|
||||
// Define the SQL query for inserting a new record in the movies table and returning
|
||||
// the system-generated data.
|
||||
query := `
|
||||
INSERT INTO movies (title, year, runtime, genres)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, created_at, version`
|
||||
|
||||
// Create an args slice containing the values for the placeholder parameters from
|
||||
// the movie struct. Declaring this slice immediately next to our SQL query helps to
|
||||
// make it nice and clear *what values are being used where* in the query.
|
||||
args := []interface{}{movie.Title, movie.Year, movie.Runtime, pq.Array(movie.Genres)}
|
||||
|
||||
// Use the QueryRow() method to execute the SQL query on our connection pool,
|
||||
// passing in the args slice as a variadic parameter and scanning the system-
|
||||
// generated id, created_at and version values into the movie struct.
|
||||
return m.DB.QueryRow(query, args...).Scan(&movie.ID, &movie.CreatedAt, &movie.Version)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
type Movie struct {
|
||||
ID int64 `json:"id"`
|
||||
CreatedAt time.Time `json:"-"` // Use the - directive
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS movies;
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE IF NOT EXISTS movies (
|
||||
id bigserial PRIMARY KEY,
|
||||
created_at timestamp(0) with time zone NOT NULL DEFAULT NOW(),
|
||||
title text NOT NULL,
|
||||
year integer NOT NULL,
|
||||
runtime integer NOT NULL,
|
||||
genres text[] NOT NULL,
|
||||
version integer NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE movies DROP CONSTRAINT IF EXISTS movies_runtime_check;
|
||||
|
||||
ALTER TABLE movies DROP CONSTRAINT IF EXISTS movies_year_check;
|
||||
|
||||
ALTER TABLE movies DROP CONSTRAINT IF EXISTS genres_length_check;
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE movies ADD CONSTRAINT movies_runtime_check CHECK (runtime >= 0);
|
||||
|
||||
ALTER TABLE movies ADD CONSTRAINT movies_year_check CHECK (year BETWEEN 1888 AND date_part('year', now()));
|
||||
|
||||
ALTER TABLE movies ADD CONSTRAINT genres_length_check CHECK (array_length(genres, 1) BETWEEN 1 AND 5);
|
||||
|
||||
Reference in New Issue
Block a user