Introduction to GORM
Learning Objectives: By the end of this lesson, you will be able to:
- Understand what GORM is and why it’s the most popular ORM for Go
- Explain Object-Relational Mapping (ORM) concepts and benefits
- Set up a Go project with GORM using Go modules
- Connect to different database systems (SQLite, PostgreSQL, MySQL)
- Create your first model and perform basic database operations
- Understand GORM’s architecture and design principles
1. Welcome and Introduction (5 minutes)
Icebreaker Activity
Have you ever found yourself writing repetitive SQL queries for basic CRUD operations? Or struggled to keep your Go structs in sync with your database schema? Perhaps you’ve wondered how large-scale Go applications manage thousands of database operations without descending into chaos? Today, we’re diving into GORM, Go’s most powerful and widely-used Object-Relational Mapping library that will transform how you interact with databases.
Lesson Overview
- Duration: 90 minutes
- Prerequisites: Basic Go syntax, structs, interfaces, basic SQL knowledge
- Materials Needed: Go 1.20+ for the SQLite driver v1.6.0 used here (GORM’s core supports Go 1.18+); text editor or IDE; Gin integration lessons use Go 1.25+
2. Learning Objectives and Session Outline (5 minutes)
What We’ll Cover Today:
- ORM Fundamentals - Understanding Object-Relational Mapping concepts
- GORM Introduction - What makes GORM special in the Go ecosystem
- Project Setup - Installing GORM and database drivers
- Database Connection - Connecting to different database systems
- First Model - Creating and using your first GORM model
- Hands-on practice with basic database operations
Success Criteria:
By the end of this session, you should be able to set up a Go project with GORM, connect to a database, define models, and perform basic CRUD operations.
3. Foundation Knowledge (15-20 minutes)
Core Concepts
What is an ORM?
Object-Relational Mapping (ORM) is a programming technique that creates a bridge between object-oriented programming languages and relational databases. Instead of writing raw SQL queries, you work with objects in your programming language, and the ORM translates these operations into appropriate database queries.
Real-World Analogy: Think of an ORM as a translator between two people who speak different languages. Your Go code speaks in objects, structs, and methods, while the database speaks in tables, rows, and SQL. The ORM ensures both parties understand each other perfectly without either needing to learn the other’s language.
Why This Matters: Without an ORM, developers spend significant time writing, debugging, and maintaining SQL queries. ORMs automate this process, reduce errors, prevent SQL injection attacks, and make your codebase more maintainable and portable across different database systems.
What is GORM?
GORM (Go Object Relational Mapper) is a developer-friendly ORM library for Go. It’s designed with simplicity and performance in mind, providing a clean API for database operations while supporting advanced features like:
- Auto migrations
- Associations (Has One, Has Many, Belongs To, Many To Many)
- Hooks (Before/After Create/Update/Delete)
- Preloading (Eager Loading)
- Transactions
- Composite Primary Keys
- SQL Builder
- Auto timestamps
Why Choose GORM?
- Developer Friendly: Intuitive API that feels natural to Go developers
- Full Featured: Supports nearly every feature you need from an ORM
- Extensive Documentation: Comprehensive docs with examples
- Active Community: Large community with continuous improvements
- Database Agnostic: Works with PostgreSQL, MySQL, SQLite, SQL Server
- Performance: Optimized queries with caching and connection pooling
Key Terminology
- Model: A Go struct that represents a database table
- Migration: The process of creating or modifying database schemas
- Association: Relationships between models (foreign keys)
- Hook: Callback functions that execute during model lifecycle events
- Preloading: Loading associated records in advance to avoid N+1 queries
- Transaction: A group of database operations that succeed or fail together
4. Detailed Explanation and Examples (25-30 minutes)
Understanding GORM Architecture
GORM follows a layered architecture that separates concerns effectively:
- API Layer: The interface you interact with (DB, Model methods)
- Callback Layer: Handles hooks and plugins
- Parser Layer: Translates Go structures to SQL
- Connector Layer: Database-specific drivers and connections
The GORM Request Flow
- Model Definition: Define Go structs with GORM tags
- Query Building: Use GORM methods to build queries
- Callback Execution: Pre and post hooks execute
- SQL Generation: GORM generates optimized SQL
- Database Execution: SQL executes on the database
- Result Mapping: Database results map back to Go structs
Practical Applications
GORM excels in several scenarios:
- Web Applications: Backend APIs with complex data models
- Microservices: Data layer for service-oriented architectures
- CLI Tools: Database management and data processing
- Background Jobs: Batch processing and data migrations
- Admin Panels: CRUD interfaces for data management
Common Misconceptions
-
Misconception: “ORMs are always slower than raw SQL” Reality: GORM generates optimized queries and supports raw SQL when needed. For most applications, the performance difference is negligible.
-
Misconception: “ORMs hide SQL, so you don’t need to understand it” Reality: Understanding SQL helps you use GORM more effectively, especially for complex queries and debugging.
-
Misconception: “GORM forces you into specific patterns” Reality: GORM is flexible and supports various architectural patterns and custom implementations.
5. Live Coding Demonstration (20-25 minutes)
Example 1: Setting Up Your First GORM Project
First, let’s create a new Go project and install GORM:
# Create project directory
mkdir gorm-intro
cd gorm-intro
# Initialize Go module
go mod init gorm-intro
# Install GORM and SQLite driver
go get gorm.io/gorm@v1.31.2 gorm.io/driver/sqlite@v1.6.0
Now, let’s create our first GORM application:
// main.go
package main
import (
"fmt"
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// Product is our first GORM model
type Product struct {
ID uint `gorm:"primaryKey"`
Code string `gorm:"size:100;not null"`
Price uint
}
func main() {
// Connect to SQLite database (creates file if not exists)
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
// Auto migrate the schema
err = db.AutoMigrate(&Product{})
if err != nil {
log.Fatal("Failed to migrate:", err)
}
fmt.Println("Database connected and migrated successfully!")
// Create a product
product := Product{Code: "D42", Price: 100}
result := db.Create(&product)
if result.Error != nil {
log.Fatal("Failed to create product:", result.Error)
}
fmt.Printf("Created product with ID: %d\n", product.ID)
// Read the product
var fetchedProduct Product
if err := db.First(&fetchedProduct, product.ID).Error; err != nil { log.Fatal(err) }
fmt.Printf("Found product: %+v\n", fetchedProduct)
// Update the product
if err := db.Model(&fetchedProduct).Update("Price", 200).Error; err != nil { log.Fatal(err) }
// Read back the stored value; do not assume Update mutates the Go struct.
if err := db.First(&fetchedProduct, product.ID).Error; err != nil { log.Fatal(err) }
fmt.Printf("Updated price to: %d\n", fetchedProduct.Price)
// Delete the product
if err := db.Delete(&fetchedProduct).Error; err != nil { log.Fatal(err) }
fmt.Println("Product deleted")
}
Explanation:
Productdefines GORM model fields and constraints.gorm.Open()opens the database;AutoMigrate()adds the required schema.Create,First,Update, andDeleteperform CRUD; check.Erroron each finisher.- Requery after an update if you need to display the persisted value.
Current GORM alternative (v1.30+): The course uses the supported traditional *gorm.DB API. New code may also use the generics API, which returns errors directly and takes a context.Context:
// With the same db and Product model defined above:
ctx := context.Background() // import "context" (inside a function)
product, err := gorm.G[Product](db).Where("code = ?", "D42").First(ctx)
if err != nil { log.Fatal(err) }
fmt.Println(product.Code)
Example 2: Connecting to Different Databases
GORM supports multiple database systems. Here’s how to connect to each:
package main
import (
"fmt"
"log"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/driver/sqlserver"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func connectSQLite() (*gorm.DB, error) {
return gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
}
func connectPostgreSQL() (*gorm.DB, error) {
dsn := "host=localhost user=postgres password=secret dbname=myapp port=5432 sslmode=disable TimeZone=Asia/Jakarta"
return gorm.Open(postgres.Open(dsn), &gorm.Config{})
}
func connectMySQL() (*gorm.DB, error) {
dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
return gorm.Open(mysql.Open(dsn), &gorm.Config{})
}
func connectSQLServer() (*gorm.DB, error) {
dsn := "sqlserver://user:password@localhost:1433?database=dbname"
return gorm.Open(sqlserver.Open(dsn), &gorm.Config{})
}
func main() {
// Example: Connect with custom configuration
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info), // Enable SQL logging
})
if err != nil {
log.Fatal("Failed to connect:", err)
}
// Get the underlying sql.DB for connection pool configuration
sqlDB, err := db.DB()
if err != nil {
log.Fatal("Failed to get sql.DB:", err)
}
// Set connection pool settings
sqlDB.SetMaxIdleConns(10) // Maximum idle connections
sqlDB.SetMaxOpenConns(100) // Maximum open connections
sqlDB.SetConnMaxLifetime(3600) // Maximum connection lifetime in seconds
fmt.Println("Database connected with custom configuration!")
}
Key Points:
- Each database requires its specific driver import
- Connection strings (DSN) vary by database type
gorm.Configallows customization of logger, naming strategies, and more- Access underlying
sql.DBfor connection pool management
Example 3: Working with Models and GORM Tags
package main
import (
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// User demonstrates various GORM tags and conventions
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:100;not null"`
Email string `gorm:"uniqueIndex;size:255"`
Age uint8 `gorm:"default:18"`
Birthday *time.Time `gorm:"type:date"`
Active bool `gorm:"default:true"`
Role string `gorm:"type:enum('admin','user','guest');default:'user'"`
CreatedAt time.Time // Automatically managed by GORM
UpdatedAt time.Time // Automatically managed by GORM
DeletedAt gorm.DeletedAt `gorm:"index"` // Soft delete support
}
// TableName customizes the table name
func (User) TableName() string {
return "users"
}
func main() {
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
// Auto migrate creates table with all constraints
db.AutoMigrate(&User{})
// Create user - CreatedAt and UpdatedAt are set automatically
user := User{
Name: "John Doe",
Email: "john@example.com",
Age: 25,
}
db.Create(&user)
// Update user - UpdatedAt is updated automatically
db.Model(&user).Update("Name", "John Smith")
// Soft delete - sets DeletedAt instead of actually deleting
db.Delete(&user)
// Query includes soft deleted records
var allUsers []User
db.Unscoped().Find(&allUsers)
}
Debugging Demonstration
Show common errors and how to fix them:
// Common mistake: Not checking for errors
db.Create(&product) // ERROR: Not handling potential errors
// Corrected version
result := db.Create(&product)
if result.Error != nil {
log.Printf("Error creating product: %v", result.Error)
return
}
fmt.Printf("Rows affected: %d\n", result.RowsAffected)
// Common mistake: Forgetting to pass pointer to struct
var product Product
db.First(product, 1) // ERROR: Should pass pointer
// Corrected version
db.First(&product, 1) // Correct: Pass pointer to struct
6. Hands-On Practice (30-40 minutes)
Exercise 1: Basic GORM Setup (10 minutes)
Objective: Create a project and connect to a SQLite database
Instructions:
- Create a new Go module
- Install GORM with SQLite driver
- Create a Book model with fields: ID, Title, Author, ISBN, PublishedYear
- Connect to database and run migration
- Verify the table is created
Starter Code:
package main
import (
"fmt"
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// TODO: Define Book struct with appropriate GORM tags
func main() {
// TODO: Connect to SQLite database "library.db"
// TODO: Run auto migration
fmt.Println("Database setup complete!")
}
Solution:
package main
import (
"fmt"
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type Book struct {
ID uint `gorm:"primaryKey"`
Title string `gorm:"size:255;not null"`
Author string `gorm:"size:150;not null"`
ISBN string `gorm:"uniqueIndex;size:20"`
PublishedYear int `gorm:"type:int"`
}
func main() {
db, err := gorm.Open(sqlite.Open("library.db"), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
err = db.AutoMigrate(&Book{})
if err != nil {
log.Fatal("Failed to migrate:", err)
}
fmt.Println("Database setup complete!")
}
Exercise 2: Complete CRUD Operations (15 minutes)
Objective: Implement all CRUD operations for the Book model
Requirements:
- Create function to add new books
- Create function to find book by ID
- Create function to find all books by author
- Create function to update book title
- Create function to delete a book
Hints:
- Use
db.Create()for insert - Use
db.First()for single record - Use
db.Where().Find()for filtered queries - Use
db.Model().Update()for updates - Use
db.Delete()for deletion
Exercise 3: Build a Simple Book Manager (15 minutes)
Objective: Create a command-line book management application
Challenge: Build a program that:
- Seeds the database with sample books
- Lists all books
- Searches books by author name
- Updates book prices
- Demonstrates soft delete functionality
Extension Ideas:
- Add a Genre field with validation
- Implement pagination for listing
- Add sorting options
7. Common Pitfalls and Troubleshooting (10 minutes)
Frequent Mistakes
-
Problem: “Unsupported data type” error when migrating Cause: Using unsupported Go types or missing proper type tags Solution: Check GORM documentation for supported types, use
gorm:"type:..."for custom types -
Problem: Fields not being saved to database Cause: Using unexported (lowercase) field names in structs Solution: Ensure all fields you want to persist are exported (start with uppercase)
-
Problem: “record not found” when it should exist Cause: Soft deletes filtering out records, or incorrect query conditions Solution: Use
Unscoped()to include soft deleted records, verify query conditions
Debugging Tips
- Enable GORM logger to see generated SQL:
logger.Default.LogMode(logger.Info) - Use
db.Debug()for temporary SQL logging on specific queries - Check
result.RowsAffectedto verify operations affected expected records - Verify model tags match database constraints
8. Best Practices and Professional Tips (5 minutes)
Industry Standards
- Always handle errors returned by GORM operations
- Use connection pooling for production applications
- Enable prepared statements for better security and performance
- Use migrations in production instead of AutoMigrate
- Implement proper logging for database operations
Next Steps in Your Learning Journey
- Learn about GORM associations (next lesson)
- Explore hooks and callbacks for business logic
- Study advanced querying techniques
- Practice with different database systems
- Learn about transaction management
9. Summary and Review (5 minutes)
Key Takeaways
- GORM simplifies database operations by mapping Go structs to database tables
- AutoMigrate provides easy schema management for development environments
- GORM supports multiple databases with a consistent API
- Proper error handling is essential for reliable database operations
Quick Review Questions
- What is the purpose of an ORM and how does GORM implement it?
- How do you connect to different database systems using GORM?
- What are GORM tags and how do they customize model behavior?
- Why is it important to check errors after GORM operations?
For Instructors
Teaching Tips
- Start with SQLite for simplicity - no server setup required
- Show the generated SQL to help students understand what GORM does
- Encourage students to experiment with different GORM tags
- Emphasize error handling from the beginning
- Use live database inspection tools to verify changes
Assessment Ideas
- Quiz on ORM concepts and GORM terminology
- Code review focusing on proper error handling
- Practical exercise: Build a model from a given database schema
- Discussion: When would you use GORM vs raw SQL?
Adaptation Notes
- For beginners: Spend more time on SQL basics and database concepts
- For advanced students: Introduce performance considerations early
- Online delivery: Provide pre-configured development environment
- Time management: The CRUD section can be extended for more practice