Assembling the Store
Building Web Services in Go built its Store as an interface and its first
implementation as a sync.Mutex-protected map, with a promise: "a database
is a future swap-in." Every lesson in this book since Chapter 1 has been
building the pieces of that swap — opening a connection, reading one row,
reading many, handling NULL, inserting, updating, deleting, wrapping it in
a transaction, making it cancellable. This lesson is where those pieces stop
being separate functions and become the swap-in itself.
type SQLStore struct{ db *sql.DB }
func NewSQLStore(db *sql.DB) *SQLStore { return &SQLStore{db: db} }
That's it — a struct holding the one thing every method needs, and a
constructor. Nothing about *sql.DB demanded this shape; you could just as
easily export five free functions that each take a *sql.DB as their first
argument, and every earlier exercise in this book did exactly that. The
struct exists for one reason: *SQLStore can now be a Store, the same
interface the in-memory map already satisfies. Whatever code in the wider
app was written against Store — handlers, tests, anything — doesn't
change one line to start talking to SQLite instead of a map. That's the
entire payoff of building against an interface from day one, and it only
cashes in once an implementation exists to swap in.
var _ Store = (*SQLStore)(nil)
Worth keeping as a habit even outside this exercise: a blank assignment like
this costs nothing at runtime — the compiler discards it — but it fails to
compile the moment SQLStore drifts from the interface, which is a far
better place to find that mistake than a panic from a type assertion three
layers into a request.
Nothing here is new, except the shape
Every method's body is a lesson you've already done: Get is
QueryRowContext + sql.ErrNoRows → ErrNotFound from Chapter 2. List is
the rows.Next/Scan/rows.Err loop from the same chapter. Create is
ExecContext + LastInsertId from Chapter 3. Update and Delete are
RowsAffected() == 0 → ErrNotFound, also Chapter 3. The only things new in
this lesson are the Context-suffixed calls throughout (Chapter 5) and a
Priority field threaded through every signature, because a real Task has
more than a title and a done flag.
Writing five methods in one sitting after learning them one at a time is
itself worth noticing: none of them individually is hard, but assembling
them into one coherent, interface-satisfying type is where a lot of small
correct pieces either add up to a store you'd trust in production, or don't
— because a copy-paste WHERE id = ? got the wrong variable, or Update
forgot the RowsAffected check that Create never needed. Nothing catches
that but running all five together against the same database, which is
exactly what the hidden test does.
Try it
Implement SQLStore's five methods — Create, Get, List, Update,
Delete — against the Store interface, matching web-services' contract:
ErrNotFound from a missing row, DB-assigned IDs, List ordered by id.
Practice
1. Assemble the SQL-Backed Store
IntermediateThe table already exists:
tasks (id, title, done, priority).Implement
SQLStore's five methods against theStoreinterface:Create(ctx, title, priority) (Task, error)— DB-assigns the id viaLastInsertId.Get(ctx, id) (Task, error)—ErrNotFoundif no such row.List(ctx) ([]Task, error)— all tasks, ordered by id.Update(ctx, id, title, done, priority) (Task, error)—ErrNotFoundif nothing matched.Delete(ctx, id) error—ErrNotFoundif nothing matched.
Use the
Context-suffixed methods (QueryRowContext,QueryContext,ExecContext) throughout. Wrap genuine failures with%w; returnErrNotFoundbare, not wrapped.package main import ( "context" "database/sql" "errors" "fmt" _ "modernc.org/sqlite" ) var ErrNotFound = errors.New("task not found") type Task struct { ID int64 Title string Done bool Priority int } // Store is the seam web-services' handlers were written against — an // in-memory map satisfies it today; SQLStore is the real swap-in. type Store interface { Create(ctx context.Context, title string, priority int) (Task, error) Get(ctx context.Context, id int64) (Task, error) List(ctx context.Context) ([]Task, error) Update(ctx context.Context, id int64, title string, done bool, priority int) (Task, error) Delete(ctx context.Context, id int64) error } type SQLStore struct{ db *sql.DB } func NewSQLStore(db *sql.DB) *SQLStore { return &SQLStore{db: db} } func (s *SQLStore) Create(ctx context.Context, title string, priority int) (Task, error) { // TODO: implement return Task{}, fmt.Errorf("not implemented") } func (s *SQLStore) Get(ctx context.Context, id int64) (Task, error) { // TODO: implement return Task{}, fmt.Errorf("not implemented") } func (s *SQLStore) List(ctx context.Context) ([]Task, error) { // TODO: implement return nil, fmt.Errorf("not implemented") } func (s *SQLStore) Update(ctx context.Context, id int64, title string, done bool, priority int) (Task, error) { // TODO: implement return Task{}, fmt.Errorf("not implemented") } func (s *SQLStore) Delete(ctx context.Context, id int64) error { // TODO: implement return fmt.Errorf("not implemented") } func main() {}Sign in to write and run your solution in the browser.
Sign in for hints and the solution.