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:
- Create Operations - Inserting single and multiple records
- Read Operations - Querying with conditions, ordering, and pagination
- Update Operations - Modifying existing records efficiently
- Delete Operations - Hard deletes and soft delete strategies
- Advanced Patterns - Upserts, batch operations, raw SQL
- 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 occurredRowsAffected- Number of affected rowsStatement- 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
-
Misconception: “Updates with struct will set zero values” Reality: Zero values are ignored. Use map or Select to include them.
-
Misconception: “Delete always removes records” Reality: With
DeletedAtfield, 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:
- Create a Product model with validation
- Implement all CRUD operations
- Add batch import functionality
- 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
-
Problem: Updates not saving zero values Cause: Using struct for Updates (zero values ignored) Solution: Use map or Select specific fields
-
Problem: Soft-deleted records appearing in queries Cause: Forgetting that soft delete is automatic Solution: Use Unscoped() when you need deleted records
-
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.RowsAffectedafter 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
- Create operations support single, batch, and upsert patterns
- Read operations offer flexible querying with conditions and pagination
- Update operations require careful handling of zero values
- Delete operations support both soft and hard delete strategies
Quick Review Questions
- How do you include zero values in an update operation?
- What’s the difference between First, Take, and Last?
- How do you permanently delete a soft-deleted record?
- 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