TaskFlow reads requests, but it has nowhere to keep anything yet. In this part we create the users table from the database design. First we write a model that describes a user. Then Goravel writes a migration from the model, and the migration creates the table.
A model is a Go struct that stands for one table. Create one for users:
Terminal window
gorun.artisanmake:modelUser
SUCCESSModelcreatedsuccessfully
This creates app/models/user.go:
app/models/user.go
packagemodels
import(
"github.com/goravel/framework/database/orm"
)
typeUserstruct{
orm.Model
}
orm.Model gives every model an id and two timestamps: when the row was created and when it was last updated. Goravel fills the timestamps for you when a row is saved. A model named User uses a table named users.
Email is unique, so two users can never share an email.
Role has its own type with two values, manager and member. If no role is given, the database uses member.
In code you write models.RoleManager instead of the string "manager". If you mistype RoleManager, the build fails and tells you. If you mistype "manager", it builds fine and the bug shows up later.
The GORM tags after each field describe its column: size:255 is the maximum length, and not null says it must have a value. Password will hold a hashed password, never the real one. We hash it when we build register and login.
Goravel does not create or change tables from these tags while the app runs. The tags only matter when make:migration reads them to write a migration.
MoreOther tags make:migration understands
Here is a model that tries a different tag on each field. It is only an example, so do not add it to TaskFlow:
app/models/probe.go
typeProbestruct{
orm.Model
Title string`gorm:"size:100"`
Body string`gorm:"type:text"`
Nick *string`gorm:"size:50"`
Points int`gorm:"default:0"`
Done bool`gorm:"default:false"`
Code string`gorm:"size:20;uniqueIndex"`
Status string`gorm:"size:20;index"`
Note string`gorm:"size:255;comment:shown to the user"`
table.String("note",255).Comment("shown to the user")
table.String("due_on",10).Nullable()
table.Decimal("price").Places(2).Total(10)
table.Json("meta")
table.Unique("code").Name("idx_probes_code")
table.Index("status").Name("idx_probes_status")
})
A pointer field, like *string, makes a column nullable. Every other column is not null, with or without a not null tag.
Give a decimal its size with precision and scale. If you write type:decimal(10,2) instead, the size is lost.
uniqueIndex works like unique, but the index is named idx_<table>_<column> instead of <table>_<column>_unique.
migration: takes the name of a schema builder method, so migration:json calls table.Json.
The migration
A migration is a Go file with two methods: Up makes a change to the database, and Down undoes it. You could create the table by hand in Postgres, but then it would only exist on your machine. A migration lives in the project with the code, so every copy of TaskFlow gets the same table.
Goravel can write that file for you from the model. -m User tells it which model to read. If you run it now, it fails:
Goravel only knows the models you tell it about, because Go cannot look up a type by its name while the program runs. Register User in bootstrap/app.go:
WithCallback takes a function that runs during startup, after the facades are ready. Inside it, Extend adds User to the models Goravel knows. Run the command again:
It writes the migration and adds it to the list in bootstrap/migrations.go. The file name, the struct and the signature include the time you ran the command, so yours will have a different number:
Up only creates the table if it is not there yet, and Down drops it.
Every column comes from the model:
created_at, updated_at and id come from orm.Model.
The unique email became table.Unique("email").
The role became table.Enum, with member as its default.
Only the two timestamps have Nullable(). Every other column is not null already, so the not null tags change nothing here. They only matter on a pointer field, which would otherwise be nullable. We keep them because they show that each column needs a value.
Code The code for every step of this part is in the tag part-04.
Run the migration
If Postgres is not running, start it with docker compose up -d. Then run the migrations:
Terminal window
gorun.artisanmigrate
INFORunningmigration
INFORunning:20210101000002_create_jobs_table
INFORunning:20261022221906_create_users_table
SUCCESSMigrationsuccess
The jobs migration runs too. It came with the Queue facade we picked when we created the project, and it creates two tables, jobs and failed_jobs. Our queue runs on Redis, so it never uses jobs, but Goravel still records failed jobs in failed_jobs.
Gotchadb:table says role is varchar, not an enum. On Postgres, Goravel stores an enum as a varchar column with a check that only lets the allowed values in. Try a role that is not allowed:
When a migration is wrong, you roll it back, fix it, and run it again. migrate:rollback runs Down for the last batch:
Terminal window
gorun.artisanmigrate:rollback
INFORollingbackmigration
INFORolledback:20261022221906_create_users_table
INFORolledback:20210101000002_create_jobs_table
SUCCESSMigrationrollbacksuccess
Both migrations were undone, not only the users one. A rollback undoes the whole last batch, and both migrations ran in batch 1. migrate:status now shows both as pending: