Associations and Relationships
Learning Objectives: By the end of this lesson, you will be able to:
- Understand and implement all GORM association types (Belongs To, Has One, Has Many, Many to Many)
- Define foreign keys with proper naming conventions and constraints
- Use association mode for managing related records
- Implement polymorphic associations for flexible data models
- Handle eager loading and preloading to avoid N+1 queries
- Create and manage join tables for many-to-many relationships
- Implement self-referential relationships
1. Welcome and Introduction (5 minutes)
Icebreaker Activity
Think about any social media platform - users have profiles, posts, comments, and followers. Each of these entities connects to others in specific ways. A user “has many” posts, a post “belongs to” a user, and users can “have many” followers through a many-to-many relationship. Today, we’ll learn how to model these real-world relationships in GORM, transforming complex business requirements into clean, efficient database schemas.
Lesson Overview
- Duration: 180 minutes
- Prerequisites: Lessons 1-3 completed, understanding of CRUD operations
- Materials Needed: Working GORM setup, database with existing models
2. Learning Objectives and Session Outline (5 minutes)
What We’ll Cover Today:
- Belongs To - Child pointing to parent (foreign key on current model)
- Has One - Parent pointing to single child (foreign key on other model)
- Has Many - Parent pointing to multiple children
- Many to Many - Bidirectional relationship through join table
- Polymorphic Associations - Single relationship to multiple model types
- Self-Referential - Model relating to itself
- Association management and preloading strategies
Success Criteria:
By the end of this session, you should be able to design and implement complex data models with proper relationships, manage associations efficiently, and avoid common pitfalls like N+1 queries.
3. Foundation Knowledge (15-20 minutes)
Core Concepts
Understanding Database Relationships
Database relationships model how entities connect in the real world:
- One-to-One: A user has one profile
- One-to-Many: A user has many posts
- Many-to-Many: Users have many roles, roles have many users
Real-World Analogy: Think of relationships like family connections. “Belongs To” is like a child knowing their parent. “Has One” is like having one spouse. “Has Many” is like having multiple children. “Many to Many” is like the relationship between students and courses - each student takes many courses, and each course has many students.
Why This Matters: Properly modeled relationships ensure data integrity, enable efficient queries, and make your code more maintainable. Poor relationship design leads to data anomalies, slow queries, and complex workarounds.
GORM Association Tags
GORM uses struct tags to define relationships:
| Tag | Purpose | Example |
|---|---|---|
foreignKey |
Custom foreign key field | gorm:"foreignKey:AuthorID" |
references |
Reference field in related model | gorm:"references:Code" |
constraint |
Foreign key constraint | gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL" |
many2many |
Join table name | gorm:"many2many:user_roles" |
joinForeignKey |
Foreign key in join table | gorm:"joinForeignKey:UserID" |
joinReferences |
Reference key in join table | gorm:"joinReferences:RoleID" |
polymorphic |
Polymorphic relationship | gorm:"polymorphic:Owner" |
Key Terminology
- Foreign Key: Column that references primary key of another table
- Join Table: Intermediate table for many-to-many relationships
- Eager Loading: Loading associated records in advance
- N+1 Problem: Performance issue when loading associations one-by-one
- Association Mode: GORM API for managing relationships
4. Detailed Explanation and Examples (25-30 minutes)
Relationship Type Overview
┌─────────────────────────────────────────────────────────────────────┐
│ GORM Relationship Types │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Belongs To Has One │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ Post │───▶│ User │ │ User │───▶│Profile │ │
│ │--------| │--------| │--------| │--------| │
│ │UserID │ │ ID │ │ ID │ │ UserID │ │
│ └────────┘ └────────┘ └────────┘ └────────┘ │
│ FK here Referenced Referenced FK here │
│ │
│ Has Many Many to Many │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ User │───▶│ Post │ │ User │──│user_ │──│ Role │ │
│ │--------| │--------| │--------| │roles │ │--------| │
│ │ ID │ │ UserID │ │ ID │ │--------| │ ID │ │
│ └────────┘ └────────┘ └────────┘ │ UserID │ └────────┘ │
│ Referenced FK here │ RoleID │ │
│ └────────┘ │
│ Join Table │
└─────────────────────────────────────────────────────────────────────┘
Foreign Key Conventions
GORM follows conventions for foreign key naming:
- For
Userstruct withCompany: foreign key isCompanyID - For
Userstruct withManager User: foreign key isManagerID - Override with
foreignKeytag when needed
Common Misconceptions
-
Misconception: “Belongs To and Has One are the same” Reality: They differ in where the foreign key lives. Belongs To has FK on current model; Has One has FK on related model.
-
Misconception: “GORM automatically loads related records” Reality: You must explicitly use Preload or Joins to load associations.
-
Misconception: “Many-to-many requires manual join table” Reality: GORM auto-creates join tables, but you can customize them.
5. Live Coding Demonstration (20-25 minutes)
Example 1: Belongs To Relationship
package main
import (
"fmt"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// Company is the parent entity
type Company struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:200;not null"`
Country string `gorm:"size:100"`
CreatedAt time.Time
}
// User belongs to Company
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:100;not null"`
Email string `gorm:"size:255;uniqueIndex"`
// Belongs To relationship
CompanyID uint `gorm:"index"` // Foreign key
Company Company `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL"` // Association
CreatedAt time.Time
}
func main() {
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
db.AutoMigrate(&Company{}, &User{})
// Create company first
company := Company{Name: "TechCorp", Country: "USA"}
db.Create(&company)
// Create user with company (using ID)
user1 := User{
Name: "John Doe",
Email: "john@techcorp.com",
CompanyID: company.ID,
}
db.Create(&user1)
// Create user with nested company (creates both)
user2 := User{
Name: "Jane Smith",
Email: "jane@newcorp.com",
Company: Company{
Name: "NewCorp",
Country: "Canada",
},
}
db.Create(&user2)
// Query user with company (Preload)
var fetchedUser User
db.Preload("Company").First(&fetchedUser, user1.ID)
fmt.Printf("User: %s, Company: %s\n", fetchedUser.Name, fetchedUser.Company.Name)
// Query with Joins (single query, inner join)
var users []User
db.Joins("Company").Where("companies.country = ?", "USA").Find(&users)
for _, u := range users {
fmt.Printf("User: %s, Company: %s\n", u.Name, u.Company.Name)
}
// Association Mode - Get associated company
var userCompany Company
db.Model(&user1).Association("Company").Find(&userCompany)
fmt.Printf("User's company: %s\n", userCompany.Name)
// Replace association
newCompany := Company{Name: "OtherCorp", Country: "UK"}
db.Create(&newCompany)
db.Model(&user1).Association("Company").Replace(&newCompany)
// Clear association (sets CompanyID to NULL)
db.Model(&user1).Association("Company").Clear()
}
Example 2: Has One Relationship
package main
import (
"fmt"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// User has one Profile
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:100;not null"`
Email string `gorm:"size:255;uniqueIndex"`
// Has One relationship - Profile has UserID foreign key
Profile Profile
CreatedAt time.Time
}
// Profile belongs to User
type Profile struct {
ID uint `gorm:"primaryKey"`
UserID uint `gorm:"uniqueIndex"` // Foreign key - unique for 1:1
Bio string `gorm:"type:text"`
Avatar string `gorm:"size:255"`
Website string `gorm:"size:255"`
Location string `gorm:"size:100"`
BirthDate *time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
func main() {
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
db.AutoMigrate(&User{}, &Profile{})
// Create user with profile
user := User{
Name: "Alice Johnson",
Email: "alice@example.com",
Profile: Profile{
Bio: "Software Developer",
Avatar: "https://example.com/avatar.jpg",
Location: "San Francisco",
},
}
db.Create(&user)
// Query user with profile
var fetchedUser User
db.Preload("Profile").First(&fetchedUser, user.ID)
fmt.Printf("User: %s, Bio: %s\n", fetchedUser.Name, fetchedUser.Profile.Bio)
// Update profile through association
db.Model(&user).Association("Profile").Replace(&Profile{
Bio: "Senior Software Developer",
Avatar: "https://example.com/new-avatar.jpg",
Location: "New York",
})
// Delete profile (has one association)
db.Model(&user).Association("Profile").Delete(&user.Profile)
// Clear and recreate
db.Model(&user).Association("Profile").Clear()
db.Model(&user).Association("Profile").Append(&Profile{
Bio: "Tech Lead",
})
}
Example 3: Has Many Relationship
package main
import (
"fmt"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// User has many Posts
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:100;not null"`
Email string `gorm:"size:255;uniqueIndex"`
// Has Many relationship
Posts []Post `gorm:"foreignKey:AuthorID;constraint:OnDelete:CASCADE"`
CreatedAt time.Time
}
// Post belongs to User (author)
type Post struct {
ID uint `gorm:"primaryKey"`
Title string `gorm:"size:255;not null"`
Content string `gorm:"type:text"`
Published bool `gorm:"default:false"`
// Foreign key to User
AuthorID uint `gorm:"index;not null"`
Author User `gorm:"foreignKey:AuthorID"`
// Has Many comments
Comments []Comment `gorm:"foreignKey:PostID;constraint:OnDelete:CASCADE"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
// Comment belongs to Post
type Comment struct {
ID uint `gorm:"primaryKey"`
PostID uint `gorm:"index;not null"`
UserID uint `gorm:"index;not null"`
Content string `gorm:"type:text;not null"`
CreatedAt time.Time
}
func main() {
db, _ := gorm.Open(sqlite.Open("blog.db"), &gorm.Config{})
db.AutoMigrate(&User{}, &Post{}, &Comment{})
// Create user with posts
user := User{
Name: "Bob Writer",
Email: "bob@blog.com",
Posts: []Post{
{Title: "First Post", Content: "Hello World!", Published: true},
{Title: "Second Post", Content: "More content here", Published: false},
},
}
db.Create(&user)
// Query user with all posts
var fetchedUser User
db.Preload("Posts").First(&fetchedUser, user.ID)
fmt.Printf("User: %s has %d posts\n", fetchedUser.Name, len(fetchedUser.Posts))
// Preload with conditions
db.Preload("Posts", "published = ?", true).First(&fetchedUser, user.ID)
fmt.Printf("Published posts: %d\n", len(fetchedUser.Posts))
// Preload with ordering
db.Preload("Posts", func(db *gorm.DB) *gorm.DB {
return db.Order("posts.created_at DESC").Limit(5)
}).First(&fetchedUser, user.ID)
// Nested preload (Posts with Comments)
db.Preload("Posts.Comments").First(&fetchedUser, user.ID)
// Association Mode - Count posts
count := db.Model(&user).Association("Posts").Count()
fmt.Printf("User has %d posts\n", count)
// Add new post to user
newPost := Post{Title: "Third Post", Content: "New content"}
db.Model(&user).Association("Posts").Append(&newPost)
// Remove specific posts
var postToRemove Post
db.Where("title = ?", "First Post").First(&postToRemove)
db.Model(&user).Association("Posts").Delete(&postToRemove)
// Replace all posts
db.Model(&user).Association("Posts").Replace([]Post{
{Title: "Replacement Post 1", Content: "Content 1"},
{Title: "Replacement Post 2", Content: "Content 2"},
})
// Clear all posts
db.Model(&user).Association("Posts").Clear()
}
Example 4: Many to Many Relationship
package main
import (
"fmt"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// User has many Roles, Role has many Users
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:100;not null"`
Email string `gorm:"size:255;uniqueIndex"`
// Many to Many
Roles []Role `gorm:"many2many:user_roles"`
CreatedAt time.Time
}
// Role model
type Role struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:50;uniqueIndex"`
Description string `gorm:"size:200"`
Users []User `gorm:"many2many:user_roles"` // Back reference (optional)
}
// Custom join table with additional fields
type UserRole struct {
UserID uint `gorm:"primaryKey"`
RoleID uint `gorm:"primaryKey"`
AssignedBy uint
AssignedAt time.Time `gorm:"autoCreateTime"`
ExpiresAt *time.Time
}
func main() {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil { panic(err) }
// For custom join table
if err := db.SetupJoinTable(&User{}, "Roles", &UserRole{}); err != nil { panic(err) }
if err := db.SetupJoinTable(&Role{}, "Users", &UserRole{}); err != nil { panic(err) }
if err := db.AutoMigrate(&User{}, &Role{}, &UserRole{}); err != nil { panic(err) }
// Create roles
adminRole := Role{Name: "admin", Description: "Administrator"}
userRole := Role{Name: "user", Description: "Regular User"}
editorRole := Role{Name: "editor", Description: "Content Editor"}
db.Create(&adminRole)
db.Create(&userRole)
db.Create(&editorRole)
// Create user with roles
user := User{
Name: "John Admin",
Email: "john@example.com",
Roles: []Role{adminRole, userRole},
}
db.Create(&user)
// Query user with roles
var fetchedUser User
db.Preload("Roles").First(&fetchedUser, user.ID)
fmt.Printf("User: %s, Roles: ", fetchedUser.Name)
for _, role := range fetchedUser.Roles {
fmt.Printf("%s ", role.Name)
}
fmt.Println()
// Query role with users
var fetchedRole Role
db.Preload("Users").Where("name = ?", "admin").First(&fetchedRole)
fmt.Printf("Role %s has %d users\n", fetchedRole.Name, len(fetchedRole.Users))
// Add role to user
db.Model(&user).Association("Roles").Append(&editorRole)
// Remove role from user
db.Model(&user).Association("Roles").Delete(&userRole)
// Replace all roles
db.Model(&user).Association("Roles").Replace([]Role{adminRole})
// Find users with specific role
var admins []User
db.Joins("JOIN user_roles ON user_roles.user_id = users.id").
Joins("JOIN roles ON roles.id = user_roles.role_id").
Where("roles.name = ?", "admin").
Find(&admins)
// Count roles for user
count := db.Model(&user).Association("Roles").Count()
fmt.Printf("User has %d roles\n", count)
// Clear all roles
db.Model(&user).Association("Roles").Clear()
}
Example 5: Polymorphic Associations
package main
import (
"fmt"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// Comment can belong to Post or Video (polymorphic)
type Comment struct {
ID uint `gorm:"primaryKey"`
Content string `gorm:"type:text;not null"`
// Polymorphic fields
CommentableID uint `gorm:"index"`
CommentableType string `gorm:"size:50;index"`
UserID uint `gorm:"index"`
CreatedAt time.Time
}
// Post has many polymorphic Comments
type Post struct {
ID uint `gorm:"primaryKey"`
Title string `gorm:"size:255;not null"`
Content string `gorm:"type:text"`
// Polymorphic has many
Comments []Comment `gorm:"polymorphic:Commentable"`
CreatedAt time.Time
}
// Video also has many polymorphic Comments
type Video struct {
ID uint `gorm:"primaryKey"`
Title string `gorm:"size:255;not null"`
URL string `gorm:"size:500;not null"`
Duration int // seconds
// Same polymorphic relationship
Comments []Comment `gorm:"polymorphic:Commentable"`
CreatedAt time.Time
}
// Image can have polymorphic owner (User or Company)
type Image struct {
ID uint `gorm:"primaryKey"`
URL string `gorm:"size:500;not null"`
// Polymorphic belongs to
OwnerID uint `gorm:"index"`
OwnerType string `gorm:"size:50;index"`
}
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:100"`
// Polymorphic has one
Avatar Image `gorm:"polymorphic:Owner"`
// Polymorphic has many
Photos []Image `gorm:"polymorphic:Owner"`
}
type Company struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:200"`
// Same polymorphic relationship
Logo Image `gorm:"polymorphic:Owner"`
Photos []Image `gorm:"polymorphic:Owner"`
}
func main() {
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
db.AutoMigrate(&Comment{}, &Post{}, &Video{}, &Image{}, &User{}, &Company{})
// Create post with comments
post := Post{
Title: "Great Article",
Content: "Some amazing content here",
Comments: []Comment{
{Content: "Great post!", UserID: 1},
{Content: "Thanks for sharing", UserID: 2},
},
}
db.Create(&post)
// Create video with comments
video := Video{
Title: "Tutorial Video",
URL: "https://example.com/video.mp4",
Duration: 600,
Comments: []Comment{
{Content: "Very helpful!", UserID: 1},
},
}
db.Create(&video)
// Query post with comments
var fetchedPost Post
db.Preload("Comments").First(&fetchedPost, post.ID)
fmt.Printf("Post '%s' has %d comments\n", fetchedPost.Title, len(fetchedPost.Comments))
// Query video with comments
var fetchedVideo Video
db.Preload("Comments").First(&fetchedVideo, video.ID)
fmt.Printf("Video '%s' has %d comments\n", fetchedVideo.Title, len(fetchedVideo.Comments))
// Add comment to post via association
db.Model(&post).Association("Comments").Append(&Comment{
Content: "Another comment",
UserID: 3,
})
// Query all comments for a specific type
var postComments []Comment
db.Where("commentable_type = ?", "posts").Find(&postComments)
fmt.Printf("Total post comments: %d\n", len(postComments))
// Create user with polymorphic images
user := User{
Name: "John",
Avatar: Image{URL: "https://example.com/avatar.jpg"},
Photos: []Image{
{URL: "https://example.com/photo1.jpg"},
{URL: "https://example.com/photo2.jpg"},
},
}
db.Create(&user)
// Query user with images
var fetchedUser User
db.Preload("Avatar").Preload("Photos").First(&fetchedUser, user.ID)
fmt.Printf("User %s has %d photos\n", fetchedUser.Name, len(fetchedUser.Photos))
}
Example 6: Self-Referential Relationships
package main
import (
"fmt"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// Employee with self-referential manager relationship
type Employee struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:100;not null"`
Email string `gorm:"size:255;uniqueIndex"`
Title string `gorm:"size:100"`
// Self-referential: Employee belongs to Manager (who is also an Employee)
ManagerID *uint `gorm:"index"`
Manager *Employee `gorm:"foreignKey:ManagerID"`
// Self-referential: Employee has many DirectReports
DirectReports []Employee `gorm:"foreignKey:ManagerID"`
CreatedAt time.Time
}
// Category with self-referential parent-child
type Category struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:100;not null"`
Slug string `gorm:"size:100;uniqueIndex"`
// Self-referential parent
ParentID *uint `gorm:"index"`
Parent *Category `gorm:"foreignKey:ParentID"`
// Self-referential children
Children []Category `gorm:"foreignKey:ParentID"`
CreatedAt time.Time
}
// User with self-referential followers (many-to-many)
type User struct {
ID uint `gorm:"primaryKey"`
Username string `gorm:"size:50;uniqueIndex"`
// Many-to-many self-referential
Followers []User `gorm:"many2many:user_followers;joinForeignKey:FollowingID;joinReferences:FollowerID"`
Following []User `gorm:"many2many:user_followers;joinForeignKey:FollowerID;joinReferences:FollowingID"`
}
func main() {
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
db.AutoMigrate(&Employee{}, &Category{}, &User{})
// ===== Employee Hierarchy =====
// Create CEO (no manager)
ceo := Employee{
Name: "Jane CEO",
Email: "jane@company.com",
Title: "CEO",
}
db.Create(&ceo)
// Create managers under CEO
cto := Employee{
Name: "Bob CTO",
Email: "bob@company.com",
Title: "CTO",
ManagerID: &ceo.ID,
}
cfo := Employee{
Name: "Alice CFO",
Email: "alice@company.com",
Title: "CFO",
ManagerID: &ceo.ID,
}
db.Create(&cto)
db.Create(&cfo)
// Create developers under CTO
dev1 := Employee{
Name: "Dev One",
Email: "dev1@company.com",
Title: "Senior Developer",
ManagerID: &cto.ID,
}
dev2 := Employee{
Name: "Dev Two",
Email: "dev2@company.com",
Title: "Developer",
ManagerID: &cto.ID,
}
db.Create(&dev1)
db.Create(&dev2)
// Query employee with manager
var employee Employee
db.Preload("Manager").First(&employee, dev1.ID)
fmt.Printf("%s reports to %s\n", employee.Name, employee.Manager.Name)
// Query manager with direct reports
var manager Employee
db.Preload("DirectReports").First(&manager, cto.ID)
fmt.Printf("%s has %d direct reports\n", manager.Name, len(manager.DirectReports))
// Recursive preload (limited depth)
var ceoWithTeam Employee
db.Preload("DirectReports.DirectReports").First(&ceoWithTeam, ceo.ID)
for _, report := range ceoWithTeam.DirectReports {
fmt.Printf(" %s (%d reports)\n", report.Name, len(report.DirectReports))
}
// ===== Category Tree =====
// Create root categories
electronics := Category{Name: "Electronics", Slug: "electronics"}
db.Create(&electronics)
// Create child categories
phones := Category{Name: "Phones", Slug: "phones", ParentID: &electronics.ID}
computers := Category{Name: "Computers", Slug: "computers", ParentID: &electronics.ID}
db.Create(&phones)
db.Create(&computers)
// Create grandchild categories
smartphones := Category{Name: "Smartphones", Slug: "smartphones", ParentID: &phones.ID}
laptops := Category{Name: "Laptops", Slug: "laptops", ParentID: &computers.ID}
db.Create(&smartphones)
db.Create(&laptops)
// Query category with children
var category Category
db.Preload("Children.Children").First(&category, electronics.ID)
fmt.Printf("Category: %s\n", category.Name)
for _, child := range category.Children {
fmt.Printf(" - %s\n", child.Name)
for _, grandchild := range child.Children {
fmt.Printf(" - %s\n", grandchild.Name)
}
}
// ===== User Followers (Many-to-Many Self-Reference) =====
users := []User{
{Username: "alice"},
{Username: "bob"},
{Username: "charlie"},
}
db.Create(&users)
// Alice follows Bob and Charlie
db.Model(&users[0]).Association("Following").Append(&users[1], &users[2])
// Bob follows Alice
db.Model(&users[1]).Association("Following").Append(&users[0])
// Query user with followers and following
var alice User
db.Preload("Followers").Preload("Following").First(&alice, users[0].ID)
fmt.Printf("%s follows %d users, has %d followers\n",
alice.Username, len(alice.Following), len(alice.Followers))
}
6. Hands-On Practice (30-40 minutes)
Exercise 1: E-commerce Data Model (15 minutes)
Objective: Build a complete e-commerce relationship structure
Instructions:
- Create models: Customer, Order, OrderItem, Product, Category
- Implement proper relationships between all models
- Add preloading for common query patterns
Solution:
package main
import (
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type Customer struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:100;not null"`
Email string `gorm:"size:255;uniqueIndex"`
Orders []Order `gorm:"foreignKey:CustomerID"`
CreatedAt time.Time
}
type Category struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:100;not null"`
ParentID *uint `gorm:"index"`
Parent *Category `gorm:"foreignKey:ParentID"`
Children []Category `gorm:"foreignKey:ParentID"`
Products []Product `gorm:"foreignKey:CategoryID"`
}
type Product struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:200;not null"`
Price float64 `gorm:"type:decimal(10,2);not null"`
Stock int `gorm:"default:0"`
CategoryID uint `gorm:"index"`
Category Category
OrderItems []OrderItem `gorm:"foreignKey:ProductID"`
DeletedAt gorm.DeletedAt `gorm:"index"`
}
type Order struct {
ID uint `gorm:"primaryKey"`
CustomerID uint `gorm:"index;not null"`
Customer Customer
OrderItems []OrderItem `gorm:"foreignKey:OrderID;constraint:OnDelete:CASCADE"`
Total float64 `gorm:"type:decimal(12,2)"`
Status string `gorm:"size:20;default:'pending'"`
CreatedAt time.Time
}
type OrderItem struct {
ID uint `gorm:"primaryKey"`
OrderID uint `gorm:"index;not null"`
ProductID uint `gorm:"index;not null"`
Product Product
Quantity int `gorm:"not null"`
Price float64 `gorm:"type:decimal(10,2);not null"`
}
func main() {
db, _ := gorm.Open(sqlite.Open("ecommerce.db"), &gorm.Config{})
db.AutoMigrate(&Customer{}, &Category{}, &Product{}, &Order{}, &OrderItem{})
// Query order with all related data
var order Order
db.Preload("Customer").
Preload("OrderItems.Product.Category").
First(&order, 1)
}
Exercise 2: Social Media Model (15 minutes)
Objective: Build user, post, comment, like relationships
Exercise 3: Content Management (10 minutes)
Objective: Implement polymorphic tags and media
7. Common Pitfalls and Troubleshooting (10 minutes)
Frequent Mistakes
-
Problem: N+1 query problem (slow performance) Cause: Not using Preload for associations Solution: Use Preload or Joins for associated data
-
Problem: Circular dependency in migrations Cause: Both tables referencing each other Solution: Disable foreign key constraints during migration or migrate in order
-
Problem: Association not saving Cause: Not using association mode or wrong relationship setup Solution: Verify foreign key field names and use Association() methods
Debugging Tips
- Use
db.Debug()to see generated SQL - Verify foreign key column exists with
db.Migrator().HasColumn() - Check association count with
Association("Name").Count()
8. Best Practices and Professional Tips (5 minutes)
Industry Standards
- Always index foreign key columns
- Use constraints for data integrity (OnDelete, OnUpdate)
- Preload only what you need
- Consider read replicas for heavy read operations
- Use transactions for operations affecting multiple related records
Next Steps in Your Learning Journey
- Learn advanced querying techniques
- Study transactions for complex operations
- Explore hooks for association lifecycle events
- Practice with real-world complex data models
9. Summary and Review (5 minutes)
Key Takeaways
- Belongs To places foreign key on current model
- Has One/Many places foreign key on related model
- Many to Many uses join table
- Preloading prevents N+1 queries
Quick Review Questions
- Where is the foreign key in a Belongs To relationship?
- How do you preload nested associations?
- When would you use a custom join table?
- What is the difference between Preload and Joins?
For Instructors
Teaching Tips
- Draw relationship diagrams on whiteboard
- Show actual database tables and foreign keys
- Demonstrate N+1 problem with logging enabled
- Use real-world examples from popular applications
Assessment Ideas
- Design data model from business requirements
- Optimize queries with proper preloading
- Debug relationship configuration issues
- Build API with complex nested responses
Adaptation Notes
- For beginners: Focus on basic relationships first
- For advanced: Cover polymorphic and self-referential early
- Include ERD diagrams for visual learners
- Provide pre-built database for exercises