Got some sloppy DB service support for snippets now.

drew/lets-go
Drew Bednar 3 months ago
parent 0a978da3ac
commit 1b823bf1d5

@ -91,6 +91,11 @@ Warning: http.ServeFile() does not automatically sanitize the file path. If you
The Let's-go book calls for MySQL. We use [go-sqlite3](ggithub.com/mattn/go-sqlite3) and [go-migrate]() tool to manage migrations instead. Use `make check-system-deps` to validate all tools for this repository are installed.
Packages to consider adopting include sqlx or https://github.com/blockloop/scan. Both have the potential to reduce the verbosity that comes will using the standard database/sql package.
### Nulls Gotcha
Beware of Nulls when using `DB.Scan()` To avoid issues consider using the sql.Null* types or putting a `NOT NULL` constraint in your DDL. Also a sensible Default
## Managing Dependencies

@ -0,0 +1,6 @@
package model
import "errors"
// TODO migrate this to an apperror
var ErrNoRecord = errors.New("models: no record found")

@ -0,0 +1,65 @@
// This file simply illustrates the use of transactions when performing operations on your sql db.
package model
import "database/sql"
type ExampleModel struct {
DB *sql.DB
// Prepared statements
//
// Prepared statements exist on database connections. Statement objects will therefore attempt to reuse the connection
// object from the connection pool that the statement was created on. If the connection was Closed or in use, it will
// be re-prepared on a new connection. This can increase load, create more connections than expected. Etc. Really its
// and optimization that you may not need to start looking at. When you do you have to look at load test data to get an
// idea for how it actually behaves.
//
// Another pattern is avoid recreated prepared statements on each invocation and instead attach them
// to the service instead. This doesn't really work well with transactions which have thier own tx.Prepare
// method
// InsertStmt *sql.Stmt
}
// func NewExampleModel(db *sql.DB) (*ExampleModel, error) {
// insertStmt, err := db.Prepare("INSERT INTO example (message, thought) VALUES (?, ?)")
// if err != nil {
// return nil, err
// }
// return &ExampleModel{DB: db, InsertStmt: insertStmt}, nil
// }
func (m *ExampleModel) ExampleTransaction() error {
// Calling the Begin() method on the connection pool creates a new sql.Tx
// object, which represents the in-progress database transaction.
tx, err := m.DB.Begin()
if err != nil {
return err
}
// Defer a call to tx.Rollback() to ensure it is always called before the
// function returns. If the transaction succeeds it will be already be
// committed by the time tx.Rollback() is called, making tx.Rollback() a
// no-op. Otherwise, in the event of an error, tx.Rollback() will rollback
// the changes before the function returns.
defer tx.Rollback()
// Call Exec() on the transaction, passing in your statement and any
// parameters. It's important to notice that tx.Exec() is called on the
// transaction object just created, NOT the connection pool. Although we're
// using tx.Exec() here you can also use tx.Query() and tx.QueryRow() in
// exactly the same way.
_, err = tx.Exec("INSERT INTO ...")
if err != nil {
return err
}
// Carry out another transaction in exactly the same way.
_, err = tx.Exec("UPDATE ...")
if err != nil {
return err
}
// If there are no errors, the statements in the transaction can be committed
// to the database with the tx.Commit() method.
err = tx.Commit()
return err
}

@ -2,15 +2,18 @@ package model
import (
"database/sql"
"errors"
"fmt"
"log/slog"
"time"
)
type Snippet struct {
ID int
Title string
Content string
ID int
// Title string
// Content string
Title sql.NullString
Content sql.NullString
CreatedAt time.Time
UpdatedAt time.Time
ExpiresAt time.Time
@ -23,6 +26,8 @@ type SnippetService struct {
// Insert inserts a new SnippetModel into the database
func (s *SnippetService) Insert(title, content string, expiresAt int) (int, error) {
slog.Debug(fmt.Sprintf("Inserting new snippet. Title: %s", title))
// Really don't prepare statements. There are a lot of gotcha's where they exist on the connection objects they were created. They can potentially
// recreate connections. It's an optimization you probably don't need at the moment.
stmt, err := s.DB.Prepare("INSERT INTO snippets (title, content, expires_at) VALUES ($1, $2, DATETIME(CURRENT_TIMESTAMP, '+' || $3 || ' DAY'))")
if err != nil {
slog.Debug("The prepared statement has an error")
@ -37,7 +42,7 @@ func (s *SnippetService) Insert(title, content string, expiresAt int) (int, erro
// will hold on to a connection until .Close() is called.
res, err := stmt.Exec(title, content, expiresAt)
if err != nil {
slog.Debug("SQL DDL returned an error.")
slog.Debug("SQL DML statement returned an error.")
return 0, err
}
@ -55,12 +60,88 @@ func (s *SnippetService) Insert(title, content string, expiresAt int) (int, erro
return int(lastId), nil
}
// Get retrieves a specific Snippet by ID
// Get retrieves a specific Snippet by ID ignoring the record if expired.
func (s *SnippetService) Get(id int) (Snippet, error) {
return Snippet{}, nil
stmt := `SELECT id, title, content, created_at, updated_at, expires_at FROM snippets
WHERE expires_at > CURRENT_TIMESTAMP AND id = $1`
// errors from DB.QueryRow() are deferred until Scan() is called.
// meaning you could also have used DB.QueryRow(...).Scan(...)
row := s.DB.QueryRow(stmt, id)
var snip Snippet
err := row.Scan(&snip.ID,
&snip.Title,
&snip.Content,
&snip.CreatedAt,
&snip.UpdatedAt,
&snip.ExpiresAt,
)
if err != nil {
slog.Debug("SQL DML statement returned an error.")
// Loop up the difference between errors.Is and errors.As
if errors.Is(err, sql.ErrNoRows) {
return Snippet{}, ErrNoRecord
} else {
return Snippet{}, err
}
}
return snip, nil
}
// Latest retrieves up to latest 10 Snippets from the database.
func (s *SnippetService) Lastest() (Snippet, error) {
return Snippet{}, nil
func (s *SnippetService) Lastest() ([]Snippet, error) {
stmt := `SELECT id, title, content, created_at, updated_at, expires_at FROM snippets
WHERE expires_at > CURRENT_TIMESTAMP ORDER BY id DESC LIMIT 10`
rows, err := s.DB.Query(stmt)
if err != nil {
return nil, err
}
// We defer rows.Close() to ensure the sql.Rows resultset is
// always properly closed before the Latest() method returns. This defer
// statement should come *after* you check for an error from the Query()
// method. Otherwise, if Query() returns an error, you'll get a panic
// trying to close a nil resultset.
defer rows.Close()
var snippets []Snippet
// Use rows.Next to iterate through the rows in the resultset. This
// prepares the first (and then each subsequent) row to be acted on by the
// rows.Scan() method. If iteration over all the rows completes then the
// resultset automatically closes itself and frees-up the underlying
// database connection.
for rows.Next() {
var snip Snippet
err := rows.Scan(&snip.ID,
&snip.Title,
&snip.Content,
&snip.CreatedAt,
&snip.UpdatedAt,
&snip.ExpiresAt,
)
if err != nil {
return nil, err
}
snippets = append(snippets, snip)
}
// When the rows.Next() loop has finished we call rows.Err() to retrieve any
// error that was encountered during the iteration. It's important to
// call this - don't assume that a successful iteration was completed
// over the whole resultset.
if err = rows.Err(); err != nil {
return nil, err
}
return snippets, nil
}

@ -2,8 +2,8 @@ package server
import (
"database/sql"
"errors"
"fmt"
"html/template"
"log/slog"
"net/http"
"strconv"
@ -55,29 +55,42 @@ func (rs *RatchetServer) home(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Server", "Go")
// Initialize a slice containing the paths to the two files. It's important
// to note that the file containing our base template must be the *first*
// file in the slice.
files := []string{
"./ui/html/base.go.tmpl",
"./ui/html/partials/nav.go.tmpl",
"./ui/html/pages/home.go.tmpl",
}
// read template file into template set.
ts, err := template.ParseFiles(files...)
if err != nil {
// Retrieve Snippets from DB
snippets, err := rs.snippetService.Lastest()
if err != err {
rs.serverError(w, r, err)
return
}
// Write template content to response body
err = ts.ExecuteTemplate(w, "base", nil)
if err != nil {
// This is the older more verbose way of doing what RatchetServer.serverError does
// rs.logger.Error(err.Error())
// http.Error(w, "Internal Server Error", http.StatusInternalServerError)
rs.serverError(w, r, err)
rs.logger.Debug(fmt.Sprintf("%d snippets retrieved", len(snippets)))
for _, snippet := range snippets {
fmt.Fprintf(w, "%+v\n", snippet)
}
// // Initialize a slice containing the paths to the two files. It's important
// // to note that the file containing our base template must be the *first*
// // file in the slice.
// files := []string{
// "./ui/html/base.go.tmpl",
// "./ui/html/partials/nav.go.tmpl",
// "./ui/html/pages/home.go.tmpl",
// }
// // read template file into template set.
// ts, err := template.ParseFiles(files...)
// if err != nil {
// rs.serverError(w, r, err)
// return
// }
// // Write template content to response body
// err = ts.ExecuteTemplate(w, "base", nil)
// if err != nil {
// // This is the older more verbose way of doing what RatchetServer.serverError does
// // rs.logger.Error(err.Error())
// // http.Error(w, "Internal Server Error", http.StatusInternalServerError)
// rs.serverError(w, r, err)
// }
}
func (rs *RatchetServer) snippetView(w http.ResponseWriter, r *http.Request) {
@ -91,16 +104,21 @@ func (rs *RatchetServer) snippetView(w http.ResponseWriter, r *http.Request) {
// Set a new cache-control header. If an existing "Cache-Control" header exists
// it will be overwritten.
w.Header().Set("Cache-Control", "public, max-age=31536000")
// msg := fmt.Sprintf("Snippet %d...", id)
// w.Write([]byte(msg))
// w.Header().Set("Cache-Control", "public, max-age=31536000")
// we can rely on the Write() interface to use a differnent
// function to write out our response
snippet, err := rs.snippetService.Get(id)
if err != nil {
rs.logger.Debug(fmt.Sprintf("Failed to retrieve an active record with id: %d", id))
if errors.Is(err, model.ErrNoRecord) {
rs.clientError(w, http.StatusNotFound)
} else {
rs.serverError(w, r, err)
}
return
}
fmt.Fprintf(w, "Snippet %d...", id)
// Write the snippet data as a plain-text HTTP response body.
fmt.Fprintf(w, "%+v", snippet)
}
// snippetCreate handles display of the form used to create snippets

Loading…
Cancel
Save