CRUD Operations

Learning Objectives: By the end of this lesson, you will be able to:

  • Create records using various GORM methods (single, batch, upsert)
  • Read records with different query methods and conditions
  • Update records selectively and efficiently
  • Delete records with both hard and soft delete strategies
  • Handle operation results, errors, and affected row counts
  • Implement batch operations for performance optimization
  • Use context for timeout and cancellation control

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Every application, from a simple todo list to a complex enterprise system, fundamentally performs four operations: Create, Read, Update, and Delete. These operations form the backbone of data manipulation. Today, we’ll master GORM’s rich API for these operations, learning not just the basics but also the nuances that distinguish a beginner from an expert.

Lesson Overview

  • Duration: 150 minutes
  • Prerequisites: Lessons 1-2 completed, models defined and migrated
  • Materials Needed: Working GORM setup with sample models

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Create Operations - Inserting single and multiple records
  2. Read Operations - Querying with conditions, ordering, and pagination
  3. Update Operations - Modifying existing records efficiently
  4. Delete Operations - Hard deletes and soft delete strategies
  5. Advanced Patterns - Upserts, batch operations, raw SQL
  6. Hands-on practice with a complete CRUD implementation

Success Criteria:

By the end of this session, you should be able to implement a fully functional data layer with proper error handling and performance optimization.

3. Foundation Knowledge (15-20 minutes)

Core Concepts

Understanding GORM’s Query Builder

GORM uses a chainable API where each method returns a *gorm.DB instance. This allows you to build complex queries step by step:

db.Model(&User{}).Where("active = ?", true).Order("created_at desc").Limit(10).Find(&users)

Real-World Analogy: Think of GORM’s query builder like building with LEGO blocks. Each method adds a piece to your query, and you can combine them in countless ways to build exactly what you need.

Why This Matters: Understanding the chainable nature helps you build dynamic queries based on runtime conditions, create reusable query scopes, and maintain clean, readable code.

The Result Object

Every GORM operation returns a *gorm.DB object containing:

  • Error - Any error that occurred
  • RowsAffected - Number of affected rows
  • Statement - Query details for debugging

Key Terminology

  • First: Find first record matching conditions
  • Find: Find all records matching conditions
  • Take: Find one record (no order guarantee)
  • Pluck: Extract single column values
  • Scan: Scan results into custom struct
  • Scope: Reusable query fragment

4. Detailed Explanation and Examples (25-30 minutes)

Create Operations Deep Dive

The example float64 price fields are teaching placeholders; use integer minor units or an exact decimal representation for financial calculations. A decimal(...) GORM tag does not change Go’s floating-point semantics.

Single Record Creation

// Basic create
user := User{Name: "John", Email: "john@example.com"}
result := db.Create(&user)
// user.ID is now populated
// result.RowsAffected == 1

Batch Creation

users := []User{
    {Name: "User1", Email: "user1@example.com"},
    {Name: "User2", Email: "user2@example.com"},
    {Name: "User3", Email: "user3@example.com"},
}
db.Create(&users) // Single INSERT statement

// Create in batches of 100
db.CreateInBatches(&users, 100)

Selective Field Creation

// Create with selected fields only
db.Select("Name", "Age").Create(&user)

// Omit specific fields
db.Omit("CreatedAt", "UpdatedAt").Create(&user)

Read Operations Deep Dive

Primary Key Lookup

var user User
db.First(&user, 10) // SELECT * FROM users WHERE id = 10 LIMIT 1
db.First(&user, "10") // Same as above
db.First(&user, []int{1, 2, 3}) // WHERE id IN (1, 2, 3)

Conditional Queries

// String condition
db.Where("name = ?", "John").First(&user)

// Struct condition (zero values ignored)
db.Where(&User{Name: "John", Age: 0}).Find(&users) // Only Name used

// Map condition (all values used)
db.Where(map[string]interface{}{"name": "John", "age": 0}).Find(&users)

Update Operations Deep Dive

Single Field Update

// Update single field
db.Model(&user).Update("name", "John Smith")

// Update with conditions
db.Model(&User{}).Where("active = ?", true).Update("level", 10)

Multiple Field Update

// Update multiple fields with struct (zero values ignored)
db.Model(&user).Updates(User{Name: "John", Age: 0}) // Only Name updated

// Update with map (all values used)
db.Model(&user).Updates(map[string]interface{}{"name": "John", "age": 0})

Common Misconceptions

  1. Misconception: “Updates with struct will set zero values” Reality: Zero values are ignored. Use map or Select to include them.

  2. Misconception: “Delete always removes records” Reality: With DeletedAt field, records are soft-deleted by default.

5. Live Coding Demonstration (20-25 minutes)

Example 1: Complete Create Operations

package main

import (
    "fmt"
    "log"
    "time"

    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

type Product struct {
    ID          uint           `gorm:"primaryKey"`
    Code        string         `gorm:"size:50;uniqueIndex"`
    Name        string         `gorm:"size:200;not null"`
    Description string         `gorm:"type:text"`
    Price       float64        `gorm:"type:decimal(10,2);not null"`
    Stock       int            `gorm:"default:0"`
    IsActive    bool           `gorm:"default:true"`
    CategoryID  uint           `gorm:"index"`
    CreatedAt   time.Time
    UpdatedAt   time.Time
    DeletedAt   gorm.DeletedAt `gorm:"index"`
}

func main() {
    db, err := gorm.Open(sqlite.Open("shop.db"), &gorm.Config{})
    if err != nil {
        log.Fatal(err)
    }
    db.AutoMigrate(&Product{})

    // ===== CREATE OPERATIONS =====

    // 1. Basic Create
    product := Product{
        Code:  "PROD-001",
        Name:  "Wireless Mouse",
        Price: 29.99,
        Stock: 100,
    }
    result := db.Create(&product)
    if result.Error != nil {
        log.Printf("Error creating product: %v", result.Error)
    }
    fmt.Printf("Created product ID: %d, Rows affected: %d\n", product.ID, result.RowsAffected)

    // 2. Create with specific fields only
    product2 := Product{
        Code:        "PROD-002",
        Name:        "Keyboard",
        Price:       59.99,
        Description: "Mechanical keyboard with RGB lighting",
        Stock:       50,
        CategoryID:  1,
    }
    db.Select("Code", "Name", "Price").Create(&product2)
    // Only Code, Name, Price are set; Description, Stock use defaults

    // 3. Create with omitted fields
    product3 := Product{
        Code:  "PROD-003",
        Name:  "Monitor",
        Price: 299.99,
        Stock: 25,
    }
    db.Omit("CreatedAt").Create(&product3)
    // CreatedAt won't be set automatically

    // 4. Batch Create
    products := []Product{
        {Code: "PROD-004", Name: "USB Cable", Price: 9.99, Stock: 500},
        {Code: "PROD-005", Name: "HDMI Cable", Price: 14.99, Stock: 300},
        {Code: "PROD-006", Name: "Power Strip", Price: 24.99, Stock: 200},
    }
    result = db.Create(&products)
    fmt.Printf("Batch created: %d products\n", result.RowsAffected)

    // 5. Create in batches (for large datasets)
    var largeDataset []Product
    for i := 100; i < 200; i++ {
        largeDataset = append(largeDataset, Product{
            Code:  fmt.Sprintf("PROD-%03d", i),
            Name:  fmt.Sprintf("Product %d", i),
            Price: float64(i) * 1.5,
            Stock: i * 10,
        })
    }
    db.CreateInBatches(&largeDataset, 50) // Insert 50 at a time

    // 6. Upsert (Create or Update)
    // Create if not exists, update if exists (based on unique constraint)
    db.Clauses(clause.OnConflict{
        Columns:   []clause.Column{{Name: "code"}},
        DoUpdates: clause.AssignmentColumns([]string{"name", "price", "stock"}),
    }).Create(&Product{
        Code:  "PROD-001",
        Name:  "Wireless Mouse Pro",
        Price: 39.99,
        Stock: 150,
    })

    // 7. Create from map
    db.Model(&Product{}).Create(map[string]interface{}{
        "Code":  "PROD-999",
        "Name":  "Mystery Product",
        "Price": 99.99,
        "Stock": 1,
    })

    fmt.Println("Create operations completed!")
}

Example 2: Comprehensive Read Operations

package main

import (
    "fmt"
    "log"

    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

type Product struct {
    ID          uint           `gorm:"primaryKey"`
    Code        string         `gorm:"size:50;uniqueIndex"`
    Name        string         `gorm:"size:200;not null"`
    Price       float64        `gorm:"type:decimal(10,2);not null"`
    Stock       int            `gorm:"default:0"`
    IsActive    bool           `gorm:"default:true"`
    CategoryID  uint           `gorm:"index"`
    CreatedAt   time.Time
    DeletedAt   gorm.DeletedAt `gorm:"index"`
}

func main() {
    db, _ := gorm.Open(sqlite.Open("shop.db"), &gorm.Config{})

    // ===== READ OPERATIONS =====

    var product Product
    var products []Product

    // 1. Find by Primary Key
    db.First(&product, 1)                    // WHERE id = 1
    db.First(&product, "id = ?", 1)          // Same with explicit condition
    db.Find(&products, []int{1, 2, 3})       // WHERE id IN (1, 2, 3)

    // 2. First vs Take vs Last
    db.First(&product)  // ORDER BY id LIMIT 1
    db.Take(&product)   // LIMIT 1 (no order)
    db.Last(&product)   // ORDER BY id DESC LIMIT 1

    // 3. Find All
    db.Find(&products)  // SELECT * FROM products

    // 4. Conditions with Where
    // String conditions
    db.Where("price > ?", 50).Find(&products)
    db.Where("name LIKE ?", "%Mouse%").Find(&products)
    db.Where("category_id IN ?", []uint{1, 2, 3}).Find(&products)
    db.Where("is_active = ? AND stock > ?", true, 0).Find(&products)

    // Struct conditions (zero values ignored!)
    db.Where(&Product{CategoryID: 1, IsActive: true}).Find(&products)
    // Note: IsActive: false would be ignored

    // Map conditions (zero values included)
    db.Where(map[string]interface{}{
        "category_id": 1,
        "is_active":   false, // This IS included
    }).Find(&products)

    // 5. Not and Or conditions
    db.Not("category_id = ?", 1).Find(&products)
    db.Where("price > ?", 50).Or("stock < ?", 10).Find(&products)

    // 6. Ordering
    db.Order("price desc").Find(&products)
    db.Order("category_id asc, price desc").Find(&products)
    db.Order("CASE WHEN stock = 0 THEN 1 ELSE 0 END, price").Find(&products)

    // 7. Pagination with Limit and Offset
    page := 2
    pageSize := 10
    db.Limit(pageSize).Offset((page - 1) * pageSize).Find(&products)

    // 8. Selecting Specific Columns
    db.Select("id", "name", "price").Find(&products)
    db.Select("id, name, price").Find(&products) // String format

    // 9. Distinct
    var categories []uint
    db.Model(&Product{}).Distinct().Pluck("category_id", &categories)

    // 10. Pluck - extract single column
    var names []string
    db.Model(&Product{}).Pluck("name", &names)

    var prices []float64
    db.Model(&Product{}).Where("is_active = ?", true).Pluck("price", &prices)

    // 11. Count
    var count int64
    db.Model(&Product{}).Where("is_active = ?", true).Count(&count)
    fmt.Printf("Active products: %d\n", count)

    // 12. Group By and Having
    type CategorySummary struct {
        CategoryID uint
        TotalStock int
        AvgPrice   float64
        Count      int64
    }
    var summaries []CategorySummary
    db.Model(&Product{}).
        Select("category_id, SUM(stock) as total_stock, AVG(price) as avg_price, COUNT(*) as count").
        Group("category_id").
        Having("COUNT(*) > ?", 5).
        Scan(&summaries)

    // 13. Joins (basic example, more in associations lesson)
    type ProductWithCategory struct {
        ProductID    uint
        ProductName  string
        CategoryName string
    }
    var results []ProductWithCategory
    db.Table("products").
        Select("products.id as product_id, products.name as product_name, categories.name as category_name").
        Joins("LEFT JOIN categories ON categories.id = products.category_id").
        Scan(&results)

    // 14. Subqueries
    subQuery := db.Model(&Product{}).Select("AVG(price)")
    db.Where("price > (?)", subQuery).Find(&products)

    // 15. Raw SQL
    db.Raw("SELECT * FROM products WHERE price > ? AND is_active = ?", 50, true).Scan(&products)

    // 16. First or Not Found handling
    result := db.First(&product, 999)
    if result.Error != nil {
        if errors.Is(result.Error, gorm.ErrRecordNotFound) {
            fmt.Println("Product not found")
        } else {
            log.Printf("Error: %v", result.Error)
        }
    }

    // 17. FirstOrInit - Initialize if not found (doesn't save)
    db.FirstOrInit(&product, Product{Code: "NEW-001"})
    // Product initialized with Code: "NEW-001" if not found

    // 18. FirstOrCreate - Create if not found
    db.FirstOrCreate(&product, Product{Code: "NEW-002"})
    // Creates record if not found

    // 19. Include soft-deleted records
    db.Unscoped().Find(&products) // Includes deleted records

    fmt.Println("Read operations completed!")
}

Example 3: Update and Delete Operations

package main

import (
    "fmt"
    "time"

    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    "gorm.io/gorm/clause"
)

type Product struct {
    ID        uint           `gorm:"primaryKey"`
    Code      string         `gorm:"size:50;uniqueIndex"`
    Name      string         `gorm:"size:200;not null"`
    Price     float64        `gorm:"type:decimal(10,2);not null"`
    Stock     int            `gorm:"default:0"`
    IsActive  bool           `gorm:"default:true"`
    UpdatedAt time.Time
    DeletedAt gorm.DeletedAt `gorm:"index"`
}

func main() {
    db, _ := gorm.Open(sqlite.Open("shop.db"), &gorm.Config{})
    db.AutoMigrate(&Product{})

    // ===== UPDATE OPERATIONS =====

    var product Product
    db.First(&product, 1)

    // 1. Update single column
    db.Model(&product).Update("name", "Updated Name")
    // UPDATE products SET name='Updated Name', updated_at=... WHERE id=1

    // 2. Update single column with conditions
    db.Model(&Product{}).Where("is_active = ?", false).Update("price", 0)

    // 3. Update multiple columns with struct (zero values ignored!)
    db.Model(&product).Updates(Product{
        Name:  "New Name",
        Price: 199.99,
        Stock: 0, // WARNING: This is ignored because it's a zero value
    })

    // 4. Update with map (zero values included)
    db.Model(&product).Updates(map[string]interface{}{
        "name":  "Another Name",
        "price": 249.99,
        "stock": 0, // This WILL be updated to 0
    })

    // 5. Select specific fields to update
    db.Model(&product).Select("name", "price").Updates(Product{
        Name:     "Selected Update",
        Price:    99.99,
        IsActive: false, // Not updated because not selected
    })

    // 6. Update with expression
    db.Model(&product).Update("stock", gorm.Expr("stock + ?", 10))
    // stock = stock + 10

    // 7. Batch update with conditions
    db.Model(&Product{}).
        Where("is_active = ? AND stock < ?", true, 10).
        Updates(map[string]interface{}{"is_active": false})

    // 8. Update returning affected rows
    result := db.Model(&Product{}).Where("price < ?", 10).Update("price", 10)
    fmt.Printf("Updated %d rows\n", result.RowsAffected)

    // 9. Update all (use with caution!)
    // db.Model(&Product{}).Where("1 = 1").Update("is_active", true)

    // 10. Update with SQL expression
    db.Model(&product).UpdateColumn("price", gorm.Expr("price * ?", 1.1))
    // Increase price by 10%

    // 11. Update without hooks (UpdateColumn/UpdateColumns)
    db.Model(&product).UpdateColumn("price", 99.99)
    // Skips hooks, doesn't update updated_at

    // 12. Upsert (Update or Insert)
    db.Clauses(clause.OnConflict{
        Columns:   []clause.Column{{Name: "code"}},
        DoUpdates: clause.AssignmentColumns([]string{"name", "price"}),
    }).Create(&Product{
        Code:  "PROD-001",
        Name:  "Upserted Product",
        Price: 149.99,
    })

    // ===== DELETE OPERATIONS =====

    // 1. Delete by primary key
    db.Delete(&Product{}, 1)
    // DELETE FROM products WHERE id = 1

    // 2. Delete with condition
    db.Delete(&Product{}, "code = ?", "PROD-999")
    // DELETE FROM products WHERE code = 'PROD-999'

    // 3. Delete by struct
    db.Delete(&product)
    // DELETE FROM products WHERE id = <product.ID>

    // 4. Batch delete
    db.Where("is_active = ?", false).Delete(&Product{})
    // DELETE FROM products WHERE is_active = false

    // 5. Delete with slice of IDs
    db.Delete(&Product{}, []int{1, 2, 3})
    // DELETE FROM products WHERE id IN (1, 2, 3)

    // 6. Soft Delete (when model has DeletedAt field)
    // By default, Delete() sets DeletedAt instead of removing record
    db.Delete(&product)
    // UPDATE products SET deleted_at = NOW() WHERE id = ?

    // 7. Permanently delete (hard delete)
    db.Unscoped().Delete(&product)
    // DELETE FROM products WHERE id = ?

    // 8. Find soft-deleted records
    var deletedProducts []Product
    db.Unscoped().Where("deleted_at IS NOT NULL").Find(&deletedProducts)

    // 9. Restore soft-deleted record
    db.Model(&product).Unscoped().Update("deleted_at", nil)

    // 10. Delete with returning affected rows
    result = db.Where("stock = ?", 0).Delete(&Product{})
    fmt.Printf("Deleted %d records\n", result.RowsAffected)

    // 11. Delete hook - block delete under conditions
    // Implement in model: BeforeDelete(tx *gorm.DB) error

    // 12. Cascade delete considerations
    // Use foreign key constraints or handle in application logic

    fmt.Println("Update and Delete operations completed!")
}

Example 4: Advanced Patterns and Best Practices

package main

import (
    "context"
    "fmt"
    "time"

    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

type Product struct {
    ID        uint           `gorm:"primaryKey"`
    Code      string         `gorm:"size:50;uniqueIndex"`
    Name      string         `gorm:"size:200;not null"`
    Price     float64        `gorm:"type:decimal(10,2);not null"`
    Stock     int            `gorm:"default:0"`
    IsActive  bool           `gorm:"default:true"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt gorm.DeletedAt `gorm:"index"`
}

// Repository pattern for clean code
type ProductRepository struct {
    db *gorm.DB
}

func NewProductRepository(db *gorm.DB) *ProductRepository {
    return &ProductRepository{db: db}
}

// Create with error handling
func (r *ProductRepository) Create(ctx context.Context, product *Product) error {
    return r.db.WithContext(ctx).Create(product).Error
}

// FindByID with not found handling
func (r *ProductRepository) FindByID(ctx context.Context, id uint) (*Product, error) {
    var product Product
    err := r.db.WithContext(ctx).First(&product, id).Error
    if err != nil {
        return nil, err
    }
    return &product, nil
}

// FindAll with pagination
func (r *ProductRepository) FindAll(ctx context.Context, page, pageSize int) ([]Product, int64, error) {
    var products []Product
    var total int64

    // Get total count
    if err := r.db.WithContext(ctx).Model(&Product{}).Count(&total).Error; err != nil {
        return nil, 0, err
    }

    // Get paginated results
    offset := (page - 1) * pageSize
    err := r.db.WithContext(ctx).
        Offset(offset).
        Limit(pageSize).
        Order("created_at desc").
        Find(&products).Error

    return products, total, err
}

// FindByConditions with dynamic filters
func (r *ProductRepository) FindByConditions(ctx context.Context, filters map[string]interface{}) ([]Product, error) {
    var products []Product
    query := r.db.WithContext(ctx)

    // Build dynamic query
    if code, ok := filters["code"]; ok {
        query = query.Where("code = ?", code)
    }
    if minPrice, ok := filters["min_price"]; ok {
        query = query.Where("price >= ?", minPrice)
    }
    if maxPrice, ok := filters["max_price"]; ok {
        query = query.Where("price <= ?", maxPrice)
    }
    if isActive, ok := filters["is_active"]; ok {
        query = query.Where("is_active = ?", isActive)
    }
    if name, ok := filters["name"]; ok {
        query = query.Where("name LIKE ?", "%"+name.(string)+"%")
    }

    err := query.Find(&products).Error
    return products, err
}

// Update with validation
func (r *ProductRepository) Update(ctx context.Context, id uint, updates map[string]interface{}) error {
    result := r.db.WithContext(ctx).Model(&Product{}).Where("id = ?", id).Updates(updates)
    if result.Error != nil {
        return result.Error
    }
    if result.RowsAffected == 0 {
        return gorm.ErrRecordNotFound
    }
    return nil
}

// SoftDelete
func (r *ProductRepository) SoftDelete(ctx context.Context, id uint) error {
    result := r.db.WithContext(ctx).Delete(&Product{}, id)
    if result.Error != nil {
        return result.Error
    }
    if result.RowsAffected == 0 {
        return gorm.ErrRecordNotFound
    }
    return nil
}

// HardDelete
func (r *ProductRepository) HardDelete(ctx context.Context, id uint) error {
    return r.db.WithContext(ctx).Unscoped().Delete(&Product{}, id).Error
}

// Batch operations
func (r *ProductRepository) BatchCreate(ctx context.Context, products []Product) error {
    return r.db.WithContext(ctx).CreateInBatches(&products, 100).Error
}

func (r *ProductRepository) BatchUpdate(ctx context.Context, ids []uint, updates map[string]interface{}) (int64, error) {
    result := r.db.WithContext(ctx).Model(&Product{}).Where("id IN ?", ids).Updates(updates)
    return result.RowsAffected, result.Error
}

func main() {
    db, _ := gorm.Open(sqlite.Open("shop.db"), &gorm.Config{})
    db.AutoMigrate(&Product{})

    repo := NewProductRepository(db)

    // Context with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    // Create
    product := &Product{Code: "NEW-001", Name: "New Product", Price: 99.99}
    if err := repo.Create(ctx, product); err != nil {
        fmt.Printf("Create error: %v\n", err)
    }

    // Find by ID
    found, err := repo.FindByID(ctx, product.ID)
    if err != nil {
        fmt.Printf("Find error: %v\n", err)
    } else {
        fmt.Printf("Found: %+v\n", found)
    }

    // Find with filters
    products, err := repo.FindByConditions(ctx, map[string]interface{}{
        "min_price": 50.0,
        "is_active": true,
    })
    fmt.Printf("Filtered products: %d\n", len(products))

    // Paginated list
    products, total, _ := repo.FindAll(ctx, 1, 10)
    fmt.Printf("Page 1: %d items, Total: %d\n", len(products), total)
}

6. Hands-On Practice (30-40 minutes)

Exercise 1: Product Inventory System (15 minutes)

Objective: Build complete CRUD for product inventory

Instructions:

  1. Create a Product model with validation
  2. Implement all CRUD operations
  3. Add batch import functionality
  4. Implement soft delete with restore capability

Solution:

package main

import (
    "context"
    "errors"
    "fmt"
    "time"

    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

type Product struct {
    ID          uint           `gorm:"primaryKey"`
    SKU         string         `gorm:"size:50;uniqueIndex;not null"`
    Name        string         `gorm:"size:200;not null"`
    Description string         `gorm:"type:text"`
    Price       float64        `gorm:"type:decimal(10,2);not null;check:price >= 0"`
    Stock       int            `gorm:"not null;default:0;check:stock >= 0"`
    CategoryID  *uint          `gorm:"index"`
    IsActive    bool           `gorm:"default:true;index"`
    CreatedAt   time.Time
    UpdatedAt   time.Time
    DeletedAt   gorm.DeletedAt `gorm:"index"`
}

// Validate product before save
func (p *Product) BeforeCreate(tx *gorm.DB) error {
    if p.SKU == "" {
        return errors.New("SKU is required")
    }
    if p.Name == "" {
        return errors.New("name is required")
    }
    if p.Price < 0 {
        return errors.New("price cannot be negative")
    }
    return nil
}

type ProductService struct {
    db *gorm.DB
}

func (s *ProductService) CreateProduct(ctx context.Context, p *Product) error {
    return s.db.WithContext(ctx).Create(p).Error
}

func (s *ProductService) GetProduct(ctx context.Context, id uint) (*Product, error) {
    var product Product
    err := s.db.WithContext(ctx).First(&product, id).Error
    return &product, err
}

func (s *ProductService) ListProducts(ctx context.Context, page, size int, activeOnly bool) ([]Product, int64, error) {
    var products []Product
    var total int64
    
    query := s.db.WithContext(ctx).Model(&Product{})
    if activeOnly {
        query = query.Where("is_active = ?", true)
    }
    
    query.Count(&total)
    err := query.Offset((page-1)*size).Limit(size).Order("created_at DESC").Find(&products).Error
    
    return products, total, err
}

func (s *ProductService) UpdateStock(ctx context.Context, id uint, quantity int) error {
    result := s.db.WithContext(ctx).Model(&Product{}).
        Where("id = ? AND stock + ? >= 0", id, quantity).
        Update("stock", gorm.Expr("stock + ?", quantity))
    
    if result.RowsAffected == 0 {
        return errors.New("insufficient stock or product not found")
    }
    return result.Error
}

func (s *ProductService) SoftDelete(ctx context.Context, id uint) error {
    return s.db.WithContext(ctx).Delete(&Product{}, id).Error
}

func (s *ProductService) Restore(ctx context.Context, id uint) error {
    return s.db.WithContext(ctx).Unscoped().Model(&Product{}).
        Where("id = ?", id).
        Update("deleted_at", nil).Error
}

func (s *ProductService) BatchImport(ctx context.Context, products []Product) (int64, error) {
    result := s.db.WithContext(ctx).CreateInBatches(&products, 50)
    return result.RowsAffected, result.Error
}

func main() {
    db, _ := gorm.Open(sqlite.Open("inventory.db"), &gorm.Config{})
    db.AutoMigrate(&Product{})
    
    service := &ProductService{db: db}
    ctx := context.Background()
    
    // Demo usage
    product := &Product{SKU: "LAPTOP-001", Name: "Gaming Laptop", Price: 1299.99, Stock: 10}
    service.CreateProduct(ctx, product)
    fmt.Printf("Created: %+v\n", product)
}

Exercise 2: User Management (10 minutes)

Objective: Implement user CRUD with search

Exercise 3: Order Processing (15 minutes)

Objective: Build order system with status updates

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Updates not saving zero values Cause: Using struct for Updates (zero values ignored) Solution: Use map or Select specific fields

  2. Problem: Soft-deleted records appearing in queries Cause: Forgetting that soft delete is automatic Solution: Use Unscoped() when you need deleted records

  3. Problem: Context cancellation not stopping query Cause: Not using WithContext() Solution: Always pass context for cancelable operations

Debugging Tips

  • Use db.Debug() to see generated SQL
  • Check result.RowsAffected after operations
  • Use errors.Is(err, gorm.ErrRecordNotFound) for not found checks

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Always handle errors, especially ErrRecordNotFound
  • Use context for timeout control in production
  • Implement repository pattern for testable code
  • Use transactions for multi-table operations
  • Log slow queries for performance monitoring

Next Steps in Your Learning Journey

  • Learn about GORM associations
  • Study transaction management
  • Explore query optimization techniques

9. Summary and Review (5 minutes)

Key Takeaways

  1. Create operations support single, batch, and upsert patterns
  2. Read operations offer flexible querying with conditions and pagination
  3. Update operations require careful handling of zero values
  4. Delete operations support both soft and hard delete strategies

Quick Review Questions

  1. How do you include zero values in an update operation?
  2. What’s the difference between First, Take, and Last?
  3. How do you permanently delete a soft-deleted record?
  4. Why use context with GORM operations?

For Instructors

Teaching Tips

  • Show SQL output for each operation
  • Demonstrate common mistakes in real-time
  • Use realistic data for examples
  • Emphasize error handling patterns

Assessment Ideas

  • Build complete CRUD API
  • Debug faulty update operations
  • Optimize slow queries
  • Handle edge cases (not found, duplicates)

Adaptation Notes

  • For beginners: Focus on basic operations first
  • For advanced: Cover batch optimizations
  • Add transactions preview for context