Models and Database Connection
Learning Objectives: By the end of this lesson, you will be able to:
- Define GORM models with proper conventions and naming strategies
- Use GORM struct tags to customize column definitions and constraints
- Implement embedded structs and custom data types
- Configure database connections with proper pooling and timeout settings
- Understand GORM’s built-in model conventions (timestamps, soft deletes)
- Create indexes, unique constraints, and composite keys
- Handle null values and default values properly
1. Welcome and Introduction (5 minutes)
Icebreaker Activity
Think about your favorite application - it might be a social media platform, an e-commerce site, or a project management tool. Behind the scenes, these applications have carefully designed data models that determine how information is stored and retrieved. Today, we’ll learn how to create these foundational data structures using GORM, transforming simple Go structs into powerful database tables with relationships, constraints, and behaviors.
Lesson Overview
- Duration: 120 minutes
- Prerequisites: Lesson 1 completed, basic understanding of database schemas
- Materials Needed: Go 1.20+ when using SQLite driver v1.6.0 (GORM’s core supports Go 1.18+), GORM set up, database connection working (Gin integration uses Go 1.25+)
Money fields: Some models below use float64 with a SQL decimal column for illustration. A SQL column type does not make Go floating-point arithmetic exact. For real balances, prices, or taxes, use integer minor units or a suitable decimal type with a compatible scanner/valuer.
2. Learning Objectives and Session Outline (5 minutes)
What We’ll Cover Today:
- Model Conventions - GORM’s default behaviors and naming strategies
- Struct Tags Deep Dive - Mastering GORM tags for column customization
- Embedded Structs - Reusing common fields across models
- Custom Types - Working with special data types and JSON
- Connection Management - Production-ready database configuration
- Hands-on practice building a complete data model
Success Criteria:
By the end of this session, you should be able to design and implement complex GORM models with proper constraints, relationships groundwork, and production-ready database connections.
3. Foundation Knowledge (15-20 minutes)
Core Concepts
GORM Model Conventions
GORM follows “convention over configuration” principles. By default:
- Struct name
Userbecomes tableusers(pluralized, snake_case) - Field
IDbecomes primary key columnid - Field
CreatedAtautomatically tracks creation time - Field
UpdatedAtautomatically tracks update time - Field
DeletedAtenables soft delete functionality
Real-World Analogy: Think of GORM conventions like a skilled assistant who anticipates your needs. When you name your struct “User”, the assistant knows you probably want a “users” table. When you add a “CreatedAt” field, they automatically fill it in every time you create a record.
Why This Matters: Conventions reduce boilerplate code and enforce consistency across your application. Understanding these conventions helps you work with GORM efficiently and know when to override them.
The gorm.Model Base
GORM provides a base struct that includes common fields:
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
Embedding gorm.Model gives you these four fields automatically.
Key Terminology
- Tag: Metadata attached to struct fields that GORM interprets
- Column: A database table column mapped to a struct field
- Constraint: Rules that limit values in a column (unique, not null, etc.)
- Index: Database structure that improves query performance
- Soft Delete: Marking records as deleted without physically removing them
- DSN: Data Source Name - connection string for databases
4. Detailed Explanation and Examples (25-30 minutes)
Understanding GORM Tags
GORM tags provide fine-grained control over how your struct fields map to database columns. They follow the format gorm:"tag1;tag2;tag3".
Common GORM Tags Reference
| Tag | Purpose | Example |
|---|---|---|
column |
Custom column name | gorm:"column:user_name" |
type |
Column data type | gorm:"type:varchar(100)" |
size |
Column size/length | gorm:"size:255" |
primaryKey |
Primary key | gorm:"primaryKey" |
unique |
Unique constraint | gorm:"unique" |
uniqueIndex |
Unique index | gorm:"uniqueIndex" |
index |
Create index | gorm:"index" |
not null |
Not null constraint | gorm:"not null" |
default |
Default value | gorm:"default:0" |
autoIncrement |
Auto increment | gorm:"autoIncrement" |
embedded |
Embed struct | gorm:"embedded" |
embeddedPrefix |
Prefix for embedded fields | gorm:"embeddedPrefix:author_" |
serializer |
Custom serializer | gorm:"serializer:json" |
- |
Ignore field | gorm:"-" |
-> |
Read-only field | gorm:"->" |
<- |
Write-only field | gorm:"<-" |
Naming Strategies
GORM converts Go naming conventions to database conventions by default:
type UserProfile struct { // Table: user_profiles
ID uint // Column: id
FirstName string // Column: first_name
LastName string // Column: last_name
EmailAddress string // Column: email_address
CreatedAt time.Time // Column: created_at
}
You can customize the naming strategy globally:
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: "app_", // table name prefix
SingularTable: true, // use singular table name
NoLowerCase: false, // use snake_case (default)
NameReplacer: strings.NewReplacer("CID", "Cid"), // custom replacer
},
})
Common Misconceptions
-
Misconception: “All struct fields are automatically persisted” Reality: Only exported fields (capitalized) are persisted. Use
gorm:"-"to explicitly skip fields. -
Misconception: “You must always use gorm.Model” Reality:
gorm.Modelis optional. You can define your own ID and timestamp fields with custom types. -
Misconception: “Primary key must be named ID” Reality: Any field can be a primary key with
gorm:"primaryKey"tag.
5. Live Coding Demonstration (20-25 minutes)
Example 1: Complete Model Definition
package main
import (
"database/sql"
"encoding/json"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// BaseModel defines common fields for all models (alternative to gorm.Model)
type BaseModel struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
}
// Address is embedded in User
type Address struct {
Street string `gorm:"size:200"`
City string `gorm:"size:100"`
State string `gorm:"size:50"`
PostalCode string `gorm:"size:20"`
Country string `gorm:"size:100;default:'USA'"`
}
// Settings represents JSON data stored in a single column
type Settings struct {
Theme string `json:"theme"`
Notifications bool `json:"notifications"`
Language string `json:"language"`
}
// Scan implements the sql.Scanner interface for Settings
func (s *Settings) Scan(value interface{}) error {
if value == nil {
return nil
}
bytes, ok := value.([]byte)
if !ok {
return nil
}
return json.Unmarshal(bytes, s)
}
// Value implements the driver.Valuer interface for Settings
func (s Settings) Value() (driver.Value, error) {
return json.Marshal(s)
}
// User demonstrates comprehensive model definition
type User struct {
BaseModel // Embed base fields
// Basic fields with various constraints
Username string `gorm:"size:50;uniqueIndex;not null"`
Email string `gorm:"size:255;uniqueIndex:idx_email;not null"`
Password string `gorm:"size:255;not null;->"` // Write-only
DisplayName string `gorm:"size:100"`
// Nullable fields using pointers
Phone *string `gorm:"size:20"`
Bio *string `gorm:"type:text"`
DateOfBirth *time.Time `gorm:"type:date"`
// Numeric fields with defaults
Age uint8 `gorm:"default:0"`
Points int64 `gorm:"default:0"`
Balance float64 `gorm:"type:decimal(10,2);default:0.00"`
// Boolean and enum fields
IsActive bool `gorm:"default:true;index"`
IsVerified bool `gorm:"default:false"`
Role string `gorm:"type:varchar(20);default:'user';check:role IN ('admin','user','guest')"`
Status string `gorm:"type:varchar(20);default:'pending'"`
// Embedded struct for address
Address Address `gorm:"embedded;embeddedPrefix:addr_"`
// JSON data stored as serialized column
Settings Settings `gorm:"type:json;serializer:json"`
Metadata datatypes.JSON `gorm:"type:json"` // Using GORM datatypes
// Computed/virtual field (not stored in database)
FullName string `gorm:"-"`
// Composite index
FirstName string `gorm:"size:50;index:idx_name"`
LastName string `gorm:"size:50;index:idx_name"`
}
// TableName customizes the table name
func (User) TableName() string {
return "users"
}
// BeforeCreate hook to set defaults
func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.Role == "" {
u.Role = "user"
}
return nil
}
func main() {
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
db.AutoMigrate(&User{})
}
Explanation:
- Lines 12-17: Custom base model with standard fields
- Lines 20-26: Address struct to be embedded in User
- Lines 29-33: Settings struct for JSON serialization
- Lines 36-47: Scanner and Valuer interfaces for custom JSON type
- Line 51: Embedding BaseModel gives us ID, timestamps, and soft delete
- Lines 54-56: String fields with size constraints and unique indexes
- Lines 59-61: Nullable fields using pointers
- Lines 64-66: Numeric fields with different types and defaults
- Lines 69-72: Boolean and enum-like fields with check constraints
- Line 75: Embedded Address struct with prefix for column names
- Lines 78-79: JSON data storage options
- Line 82: Ignored field not persisted to database
- Lines 85-86: Composite index on multiple fields
Example 2: Advanced Connection Configuration
package main
import (
"fmt"
"log"
"os"
"time"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// DatabaseConfig holds connection parameters
type DatabaseConfig struct {
Host string
Port int
User string
Password string
DBName string
SSLMode string
MaxIdleConns int
MaxOpenConns int
MaxLifetime time.Duration
}
// NewDatabase creates a configured database connection
func NewDatabase(config DatabaseConfig) (*gorm.DB, error) {
dsn := fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
config.Host,
config.Port,
config.User,
config.Password,
config.DBName,
config.SSLMode,
)
// Custom logger configuration
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: 200 * time.Millisecond, // Slow SQL threshold
LogLevel: logger.Info, // Log level
IgnoreRecordNotFoundError: true, // Ignore not found errors
Colorful: true, // Enable color
},
)
// Open connection with custom configuration
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: newLogger,
DisableForeignKeyConstraintWhenMigrating: false,
SkipDefaultTransaction: false,
PrepareStmt: true, // Prepared statement caching
})
if err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
// Get underlying sql.DB for connection pool configuration
sqlDB, err := db.DB()
if err != nil {
return nil, fmt.Errorf("failed to get sql.DB: %w", err)
}
// Configure connection pool
sqlDB.SetMaxIdleConns(config.MaxIdleConns)
sqlDB.SetMaxOpenConns(config.MaxOpenConns)
sqlDB.SetConnMaxLifetime(config.MaxLifetime)
sqlDB.SetConnMaxIdleTime(5 * time.Minute)
// Verify connection
if err := sqlDB.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return db, nil
}
func main() {
config := DatabaseConfig{
Host: getEnv("DB_HOST", "localhost"),
Port: 5432,
User: getEnv("DB_USER", "postgres"),
Password: getEnv("DB_PASSWORD", "secret"),
DBName: getEnv("DB_NAME", "myapp"),
SSLMode: getEnv("DB_SSLMODE", "disable"),
MaxIdleConns: 10,
MaxOpenConns: 100,
MaxLifetime: time.Hour,
}
db, err := NewDatabase(config)
if err != nil {
log.Fatal(err)
}
fmt.Println("Database connected successfully!")
_ = db // Use db for operations
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
Key Points:
- Lines 14-24: Structured configuration for database connection
- Lines 38-47: Custom logger with slow query threshold and log level
- Lines 50-55: GORM configuration with prepared statements
- Lines 63-68: Connection pool settings for production
- Lines 71-73: Connection verification with Ping
Example 3: Complex Model with Multiple Indexes
package main
import (
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// Order demonstrates complex indexes and constraints
type Order struct {
ID uint `gorm:"primaryKey"`
OrderNumber string `gorm:"size:20;uniqueIndex"`
// Foreign key (will be properly set up in associations lesson)
CustomerID uint `gorm:"index;not null"`
// Composite unique constraint
StoreID uint `gorm:"uniqueIndex:idx_store_date"`
OrderDate time.Time `gorm:"uniqueIndex:idx_store_date;type:date"`
// Indexed fields for common queries
Status string `gorm:"size:20;index:idx_status;default:'pending'"`
PaymentType string `gorm:"size:20;index:idx_payment"`
// Financial fields
Subtotal float64 `gorm:"type:decimal(12,2);not null"`
Tax float64 `gorm:"type:decimal(12,2);default:0"`
Discount float64 `gorm:"type:decimal(12,2);default:0"`
Total float64 `gorm:"type:decimal(12,2);not null"`
// Shipping info embedded
ShippingAddress Address `gorm:"embedded;embeddedPrefix:ship_"`
BillingAddress Address `gorm:"embedded;embeddedPrefix:bill_"`
// Metadata
Notes string `gorm:"type:text"`
CreatedAt time.Time
UpdatedAt time.Time
CompletedAt *time.Time
}
// Product with comprehensive constraints
type Product struct {
ID uint `gorm:"primaryKey"`
SKU string `gorm:"size:50;uniqueIndex;not null"`
Name string `gorm:"size:200;not null;index"`
Description string `gorm:"type:text"`
// Categorization with composite index for filtering
CategoryID uint `gorm:"index:idx_cat_brand"`
BrandID uint `gorm:"index:idx_cat_brand"`
// Pricing with check constraints
Price float64 `gorm:"type:decimal(10,2);not null;check:price >= 0"`
CostPrice float64 `gorm:"type:decimal(10,2);check:cost_price >= 0"`
// Inventory
StockQty int `gorm:"default:0;check:stock_qty >= 0"`
MinStockQty int `gorm:"default:0"`
// Status flags with multi-column index
IsActive bool `gorm:"default:true;index:idx_active_featured"`
IsFeatured bool `gorm:"default:false;index:idx_active_featured"`
// Dimensions (nullable)
Weight *float64 `gorm:"type:decimal(8,2)"`
Length *float64 `gorm:"type:decimal(8,2)"`
Width *float64 `gorm:"type:decimal(8,2)"`
Height *float64 `gorm:"type:decimal(8,2)"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
func main() {
db, _ := gorm.Open(sqlite.Open("shop.db"), &gorm.Config{})
// Migrate all models
db.AutoMigrate(&Order{}, &Product{})
}
Debugging Demonstration
// Common mistake: Using zero values for optional fields
type User struct {
Name string
Age int // Zero value (0) will be saved - might not be intended
Phone string // Empty string will be saved
}
// Corrected: Use pointers for truly optional/nullable fields
type User struct {
Name string
Age *int // nil means not set, 0 means actual zero
Phone *string // nil means not set
}
// Common mistake: Forgetting index on frequently queried fields
type Order struct {
CustomerID uint // No index - slow queries on large tables
Status string
}
// Corrected: Add appropriate indexes
type Order struct {
CustomerID uint `gorm:"index"`
Status string `gorm:"index"`
}
6. Hands-On Practice (30-40 minutes)
Exercise 1: Design an E-commerce Model (15 minutes)
Objective: Create a comprehensive Customer model
Instructions:
- Create a Customer model with personal info, contact details, and preferences
- Include embedded address structs for shipping and billing
- Add appropriate indexes and constraints
- Implement nullable fields correctly
- Add JSON field for preferences
Starter Code:
package main
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// TODO: Define Address struct
// TODO: Define Preferences struct with JSON serialization
// TODO: Define Customer model with all requirements
func main() {
db, _ := gorm.Open(sqlite.Open("ecommerce.db"), &gorm.Config{})
// TODO: Migrate Customer model
}
Solution:
package main
import (
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type Address struct {
Street string `gorm:"size:200"`
City string `gorm:"size:100;not null"`
State string `gorm:"size:50"`
PostalCode string `gorm:"size:20"`
Country string `gorm:"size:100;default:'USA'"`
}
type Preferences struct {
Newsletter bool `json:"newsletter"`
SMSAlerts bool `json:"sms_alerts"`
EmailAlerts bool `json:"email_alerts"`
PreferredLang string `json:"preferred_language"`
Currency string `json:"currency"`
}
type Customer struct {
ID uint `gorm:"primaryKey"`
CustomerNumber string `gorm:"size:20;uniqueIndex;not null"`
// Personal Info
FirstName string `gorm:"size:50;not null;index:idx_name"`
LastName string `gorm:"size:50;not null;index:idx_name"`
Email string `gorm:"size:255;uniqueIndex;not null"`
Phone *string `gorm:"size:20"`
DateOfBirth *time.Time `gorm:"type:date"`
// Addresses
ShippingAddress Address `gorm:"embedded;embeddedPrefix:ship_"`
BillingAddress Address `gorm:"embedded;embeddedPrefix:bill_"`
// Account Status
IsActive bool `gorm:"default:true;index"`
IsVerified bool `gorm:"default:false"`
Tier string `gorm:"size:20;default:'standard';index"` // standard, silver, gold, platinum
// Preferences
Preferences Preferences `gorm:"serializer:json"`
// Financials
TotalSpent float64 `gorm:"type:decimal(12,2);default:0"`
LoyaltyPoints int `gorm:"default:0"`
// Timestamps
CreatedAt time.Time
UpdatedAt time.Time
LastLoginAt *time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
func main() {
db, _ := gorm.Open(sqlite.Open("ecommerce.db"), &gorm.Config{})
db.AutoMigrate(&Customer{})
}
Exercise 2: Production Database Connection (10 minutes)
Objective: Create a reusable database connection module
Requirements:
- Support environment-based configuration
- Implement proper connection pooling
- Add logging configuration
- Include connection health check
Exercise 3: Multi-Model Schema (15 minutes)
Objective: Design a blog system schema
Challenge: Create models for:
- Author (with profile information)
- Post (with content and metadata)
- Tag (with name and slug)
- Include proper indexes and constraints
- Prepare for relationships (foreign key fields)
Extension Ideas:
- Add revision history model
- Implement status workflow
- Add SEO metadata fields
7. Common Pitfalls and Troubleshooting (10 minutes)
Frequent Mistakes
-
Problem: Migration fails with “duplicate column” error Cause: Running migration after renaming a field (GORM adds new, doesn’t remove old) Solution: Use manual migration for column renames, or handle in hook
-
Problem: Time fields show wrong timezone Cause: Database and application timezone mismatch Solution: Use UTC consistently, configure DSN with timezone parameter
-
Problem: Connection pool exhausted under load Cause: Not closing connections or pool too small Solution: Configure
MaxOpenConnsappropriately, ensure no connection leaks
Debugging Tips
- Use
db.Migrator().HasColumn(&User{}, "Name")to check column existence - Enable SQL logging to see generated DDL statements
- Use
db.Exec()for raw DDL when AutoMigrate is insufficient - Test migrations on a copy of production data
8. Best Practices and Professional Tips (5 minutes)
Industry Standards
- Use meaningful, consistent naming conventions
- Add indexes for all foreign keys and frequently queried columns
- Use appropriate data types (don’t use TEXT for short strings)
- Always include timestamps for audit trail
- Consider soft deletes for data that might need recovery
Next Steps in Your Learning Journey
- Learn about GORM associations and relationships
- Study CRUD operations in depth
- Explore migration strategies for production
- Practice with different database systems
9. Summary and Review (5 minutes)
Key Takeaways
- GORM conventions reduce boilerplate - naming strategies and auto-timestamps
- Tags provide fine control - customize every aspect of column definition
- Connection pooling is essential - proper configuration for production loads
- Use pointers for nullable fields - distinguish between zero values and null
Quick Review Questions
- What’s the difference between
gorm.Modeland a custom base model? - How do you create a composite index on multiple columns?
- Why use pointers for optional fields instead of zero values?
- What connection pool settings should you configure for production?
For Instructors
Teaching Tips
- Show database schema after migration to visualize the result
- Compare generated SQL across different database types
- Demonstrate connection pool behavior under load
- Use real-world examples from popular applications
Assessment Ideas
- Design a model from given business requirements
- Review existing model and suggest improvements
- Debugging exercise with common migration issues
- Performance analysis of index strategies
Adaptation Notes
- For beginners: Focus on basic tags, skip advanced features
- For advanced: Include database-specific optimizations
- Online delivery: Provide schema visualization tools
- Time management: Connection pooling section can be homework