Relations
HasMany, BelongsTo, ManyToMany.
Real domains have shapes â a customer has many invoices, an invoice belongs to a customer, a tag belongs to many products. GORM has one struct tag per relation kind. This lesson covers all three.
1. BelongsTo (the foreign key sits on this row)
type Invoice struct {ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`CustomerID uuid.UUID `gorm:"type:uuid;not null;index" json:"customer_id"`Customer Customer `gorm:"foreignKey:CustomerID" json:"customer,omitempty"`Total decimal.Decimal `gorm:"type:decimal(10,2);not null" json:"total"`CreatedAt time.Time `json:"created_at"`}
Two pieces â the FK column (CustomerID) and the relation field (Customer Customer). Use Preload to eager-load:
var invoice Invoicedb.Preload("Customer").First(&invoice, "id = ?", id)// invoice.Customer.Name is now populated
2. HasMany (the other table owns the FK)
type Customer struct {ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`Name string `gorm:"not null" json:"name"`Invoices []Invoice `gorm:"foreignKey:CustomerID" json:"invoices,omitempty"`}
Invoices []Invoice means "this customer has many invoices". The FK lives on the Invoice row. Eager-load the same way:
var c Customerdb.Preload("Invoices").First(&c, "id = ?", id)// c.Invoices is a slice
3. Many-to-Many (a join table)
type Product struct {ID uuid.UUID `gorm:"type:uuid;primaryKey"`Tags []Tag `gorm:"many2many:product_tags;" json:"tags,omitempty"`}type Tag struct {ID uuid.UUID `gorm:"type:uuid;primaryKey"`Name string `gorm:"uniqueIndex;not null"`}
GORM creates the product_tags join table for you. Add a tag:
db.Model(&product).Association("Tags").Append(&tag)
<RelationName>ID in the model and <relation_name>_id in the DB (snake_case). GORM follows this automatically; you only need foreignKey: when overriding.OnDelete behaviour
What happens when you delete a customer with invoices? Three choices:
// Cascade â delete invoices tooCustomer Customer `gorm:"foreignKey:CustomerID;constraint:OnDelete:CASCADE"`// Set null â keep invoices but null out CustomerID (need nullable FK)Customer Customer `gorm:"foreignKey:CustomerID;constraint:OnDelete:SET NULL"`// Default: restrict â refuse to delete the customer if invoices exist
Production-grade Grit projects tend to use RESTRICT + soft-delete the parent. Cascade is irreversible â pick it consciously.
Quick check
Customer Customer `gorm:"foreignKey:CustomerID"` but db.Preload("Customer").First(...) returns Customer with all-zero fields. What's most likely wrong?Try it
Add the invoice domain to your bench-api:
Customer(HasMany Invoices)Invoice(BelongsTo Customer, HasMany LineItems)LineItem(BelongsTo Invoice)
Wire each with the right FK + relation fields. Run grit migrate and confirm via GORM Studio that the FK columns exist. Paste the schema in notes.md.
What's next
Models compile, AutoMigrate runs. But should you use AutoMigrate forever? Last lesson of this chapter: when AutoMigrate is fine and when to switch to explicit migrations.
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