GORM basics
Models, tags, the convention surface.
GORM is the ORM that ships with Grit. It speaks Postgres and SQLite out of the box; you write Go structs, it writes SQL. This lesson is the 7-minute crash course you need to read every Grit model.
A model is a Go struct + tags
package modelsimport ("time""github.com/google/uuid")type Customer struct {ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`Email string `gorm:"uniqueIndex;not null" json:"email"`Name string `gorm:"not null" json:"name"`CreatedAt time.Time `json:"created_at"`UpdatedAt time.Time `json:"updated_at"`}
The tags that matter
gorm:"primaryKey"â this field is the PKgorm:"type:uuid"â column type (use uuid for IDs; Grit defaults to UUID PKs)gorm:"not null"â DB-level NOT NULL constraintgorm:"uniqueIndex"â unique index on this columngorm:"default:value"â column defaultgorm:"-"â skip this field entirely (never write to DB)json:"snake_name"â how it appears in the API response
UUID, not auto-increment
Grit uses UUIDs for primary keys, not uint auto-increment. Two reasons: (1) IDOR defence â attackers can't guess the next ID; (2) globally unique â you can generate them client-side and merge across DBs without conflict.
// Auto-generated in a BeforeCreate hookfunc (c *Customer) BeforeCreate(tx *gorm.DB) error {if c.ID == uuid.Nil {c.ID = uuid.New()}return nil}
You don't need to write this hook yourself if you use grit generate resource â it's already in the template.
CreatedAt + UpdatedAt â free
GORM auto-sets CreatedAt on insert and UpdatedAt on every save. No work on your part â just declare them with the right names and types.
DeletedAt gorm.DeletedAt `gorm:"index"` turns on soft delete. Calling db.Delete(&customer) sets the column; queries automatically exclude soft-deleted rows. Useful for audit-able domains.Reading + writing
// Createc := &Customer{Email: "alex@example.com", Name: "Alex"}db.Create(c)// Read by IDvar c Customerdb.First(&c, "id = ?", id)// Update one fielddb.Model(&c).Update("name", "Alex Doe")// Query with WHEREvar customers []Customerdb.Where("email LIKE ?", "%@example.com").Find(&customers)
Always pass arguments as the second+ parameter â never concatenate strings (covered as SQLi defence in the Concepts course).
Quick check
db.Raw("SELECT * FROM users WHERE email = '" + email + "'").Scan(&u). What's wrong?Try it
Add a Customer model to your bench-api. Then write three lines of code in main.go (or a temporary debug handler) that create one and read it back.
db.AutoMigrate(&Customer{})c := &Customer{Email: "alex@example.com", Name: "Alex"}db.Create(c)var found Customerdb.First(&found, "email = ?", "alex@example.com")log.Println("found:", found.ID, found.Name)
Paste the log output in notes.md.
What's next
Single tables are easy. Real apps have relations â Customer has many Invoices; an Invoice belongs to a Customer. Next lesson: the three relation kinds and how Grit defines each.
Spot a typo? Have an idea?
Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled â suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.
Suggest an improvement on GitHub