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:

  1. Belongs To - Child pointing to parent (foreign key on current model)
  2. Has One - Parent pointing to single child (foreign key on other model)
  3. Has Many - Parent pointing to multiple children
  4. Many to Many - Bidirectional relationship through join table
  5. Polymorphic Associations - Single relationship to multiple model types
  6. Self-Referential - Model relating to itself
  7. 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 User struct with Company: foreign key is CompanyID
  • For User struct with Manager User: foreign key is ManagerID
  • Override with foreignKey tag when needed

Common Misconceptions

  1. 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.

  2. Misconception: “GORM automatically loads related records” Reality: You must explicitly use Preload or Joins to load associations.

  3. 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:

  1. Create models: Customer, Order, OrderItem, Product, Category
  2. Implement proper relationships between all models
  3. 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

  1. Problem: N+1 query problem (slow performance) Cause: Not using Preload for associations Solution: Use Preload or Joins for associated data

  2. Problem: Circular dependency in migrations Cause: Both tables referencing each other Solution: Disable foreign key constraints during migration or migrate in order

  3. 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

  1. Belongs To places foreign key on current model
  2. Has One/Many places foreign key on related model
  3. Many to Many uses join table
  4. Preloading prevents N+1 queries

Quick Review Questions

  1. Where is the foreign key in a Belongs To relationship?
  2. How do you preload nested associations?
  3. When would you use a custom join table?
  4. 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