Skip to content

3. The model ​

A task has a name right now. You want a title and a done flag. In this chapter you reshape the table by changing a struct, and you learn what a migration is.

Where the table came from ​

Open migrations/0.0.1_create_task_table.sql:

sql
-- Generated by prea from the models: review it before committing.

create table if not exists "task" (
	"id" uuid not null default gen_random_uuid(),
	"name" text not null,
	primary key ("id")
);

-- prea:down
drop table "task";

This is a migration: a SQL file that changes the database. In the dev profile, whenever a model differs from the database, Preá writes a migration with the difference and applies it. The model is preas/task/model.go, and this file is its translation to SQL.

Two settings in config/application-dev.yaml turn this on:

yaml
database:
  migrations:
    from_models: true # a model change becomes a migration at startup...
    on_start: true # ...that is applied right away

prea new --with-db writes them for you. The base application.yaml leaves both off, so only dev writes and applies migrations. You'll see why in chapter 7.

No migration in migrations/?

If you created the project without --with-db and added the database by hand, these two lines are missing, and Preá writes nothing. Add them to config/application-dev.yaml and restart prea dev. To write a migration yourself, see Migrations.

Start the table over ​

This migration only exists on your machine, so you can still throw it away. Stop prea dev with Ctrl+C, then undo it and delete it:

bash
prea migrate down
rm migrations/0.0.1_create_task_table.sql
undid 0.0.1_create_task_table (5ms)

prea migrate down ran the part below -- prea:down, and the table is gone.

Only on your machine

Once a migration is committed and others have applied it, never edit or delete it. Write a new one instead.

Change the model ​

Replace preas/task/model.go:

go
package task

// Task is one thing to do.
type Task struct {
	ID    string `json:"id" pk:"" generated:"uuid"`
	Title string `json:"title" size:"200"`
	Done  bool   `json:"done" default:"false"`
}

func (Task) Table() string {
	return "task"
}

Each tag describes the column:

  • pk:"" makes id the primary key, and generated:"uuid" lets the database generate it.
  • size:"200" limits the title to 200 characters.
  • default:"false" makes new tasks start as not done.
  • title is not null because a string can't be nil. A *string would allow null.

Change the request bodies ​

The files in dtos/ describe what clients send. Replace preas/task/dtos/create_request.go:

go
package dtos

// CreateRequest is the body of POST /task.
type CreateRequest struct {
	Title string `json:"title" binding:"required"`
}

And preas/task/dtos/update_request.go:

go
// UpdateRequest is the body of PATCH /task/:id. Only the fields you send
// change: nil keeps the column as it is.
type UpdateRequest struct {
	Title *string `json:"title"`
	Done  *bool   `json:"done"`
}

The fields of the update are pointers on purpose. You'll see why in the next chapter.

Fix the service ​

The service still talks about Name. In preas/task/service.go, change the Create method:

go
func (s *Service) Create(ctx context.Context, req dtos.CreateRequest) (Task, error) {
	req.Name = strings.TrimSpace(req.Name) 
	if req.Name == "" { 
		return Task{}, core.NewError(http.StatusBadRequest, "name can't be blank") 
	req.Title = strings.TrimSpace(req.Title) 
	if req.Title == "" { 
		return Task{}, core.NewError(http.StatusBadRequest, "title can't be blank") 
	}
	return s.Repo.Create(ctx, req)
}

And make Update hand the request straight to the repository:

go
func (s *Service) Update(ctx context.Context, id string, req dtos.UpdateRequest) (Task, error) {
	req.Name = strings.TrimSpace(req.Name) 
	return s.Repo.Update(ctx, id, req)
}

Run it ​

bash
prea dev
level=SUCCESS msg="database: generated a migration from the models: review it before committing" file=migrations/0.0.1_create_task_table.sql
level=INFO msg="database: applied 0.0.1_create_task_table (5ms)"

A fresh 0.0.1_create_task_table.sql, now with your columns:

sql
create table if not exists "task" (
	"id" uuid not null default gen_random_uuid(),
	"title" varchar(200) not null,
	"done" bool not null default false,
	primary key ("id")
);
bash
curl -X POST localhost:8000/task -H 'Content-Type: application/json' -d '{"title": "Buy coffee"}'
json
{
    "code": 1,
    "data": {
        "id": "4caff630-444a-4a86-bb31-3b5e963cfb32",
        "title": "Buy coffee",
        "done": false
    }
}

done came from the database default.

What you learned ​

  • The model describes the table, and its tags describe the columns.
  • In dev, Preá turns model changes into migrations and applies them.
  • Review every migration; a local one can still be undone and regenerated.

Next: business rules →

Released under the MIT License.