Let's Go Database-driven responses › Designing a database model
Previous · Contents · Next
Chapter 4.5.

Designing a database model

In this chapter we’re going to sketch out a database model for our project.

If you don’t like the term model, you might want to think of it as a service layer or data access layer instead. Whatever you prefer to call it, the idea is that we will encapsulate the code for working with MySQL in a separate package to the rest of our application.

For now, we’ll create a skeleton database model and have it return a bit of dummy data. It won’t do much, but I’d like to explain the pattern before we get into the nitty-gritty of SQL queries.

Sound OK? Then let’s go ahead and create a new internal/models directory containing a snippets.go file:

$ mkdir -p internal/models
$ touch internal/models/snippets.go
04.05-01.png

Let’s open the internal/models/snippets.go file and add a new Snippet struct to represent the data for an individual snippet, along with a SnippetModel type with methods on it to access and manipulate the snippets in our database. Like so:

File: internal/models/snippets.go
package models

import (
    "database/sql"
    "time"
)

// Define a Snippet type to hold the data for an individual snippet. Notice how
// the fields of the struct correspond to the fields in our MySQL snippets
// table?
type Snippet struct {
    ID      int
    Title   string
    Content string
    Created time.Time
    Expires time.Time
}

// Define a SnippetModel type which wraps a sql.DB connection pool.
type SnippetModel struct {
    DB *sql.DB
}

// This will insert a new snippet into the database.
func (m *SnippetModel) Insert(title string, content string, expires int) (int, error) {
    return 0, nil
}

// This will return a specific snippet based on its id.
func (m *SnippetModel) Get(id int) (Snippet, error) {
    return Snippet{}, nil
}

// This will return the 10 most recently created snippets.
func (m *SnippetModel) Latest() ([]Snippet, error) {
    return nil, nil
}

Using the SnippetModel

To use this model in our handlers we need to establish a new SnippetModel struct in our main() function and then inject it as a dependency via the application struct — just like we have with our other dependencies.

Here’s how:

File: cmd/web/main.go
package main

import (
    "database/sql"
    "flag"
    "log/slog"
    "net/http"
    "os"

    // Import the models package that we just created. You need to prefix this with
    // whatever module path you set up back in chapter 02.01 (Project Setup and Creating
    // a Module) so that the import statement looks like this:
    // "{your-module-path}/internal/models". If you can't remember what module path you 
    // used, you can find it at the top of the go.mod file.
    "snippetbox.alexedwards.net/internal/models" 

    _ "github.com/go-sql-driver/mysql"
)

// Add a snippets field to the application struct. This will allow us to
// make the SnippetModel object available to our handlers.
type application struct {
    logger   *slog.Logger
    snippets *models.SnippetModel
}

func main() {
    addr := flag.String("addr", ":4000", "HTTP network address")
    dsn := flag.String("dsn", "web:pass@/snippetbox?parseTime=true", "MySQL data source name")
    flag.Parse()

    logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

    db, err := openDB(*dsn)
    if err != nil {
        logger.Error(err.Error())
        os.Exit(1)
    }
    defer db.Close()

    // Initialize a models.SnippetModel instance containing the connection pool
    // and add it to the application dependencies.
    app := &application{
        logger:   logger,
        snippets: &models.SnippetModel{DB: db},
    }

    logger.Info("starting server", "addr", *addr)

    err = http.ListenAndServe(*addr, app.routes())
    logger.Error(err.Error())
    os.Exit(1)
}

...

Additional information

Benefits of this structure

If you take a step back, you might be able to see a few benefits of setting up our project in this way: