Skip to main content

GORM

GORM is a fantastic ORM (Object-Relational Mapping) library for Golang, aiming to be developer-friendly. It provides a full-featured ORM with associations, hooks, transactions, migrations, and more.

Installation

go get -u gorm.io/gorm

Database Drivers

# PostgreSQL
go get -u gorm.io/driver/postgres

# MySQL
go get -u gorm.io/driver/mysql

# SQLite
go get -u gorm.io/driver/sqlite

# SQL Server
go get -u gorm.io/driver/sqlserver

# ClickHouse
go get -u gorm.io/driver/clickhouse

Quick Start

Database Connection

package main

import (
"log"

"gorm.io/driver/postgres"
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)

// PostgreSQL
func connectPostgreSQL() *gorm.DB {
dsn := "host=localhost user=username password=password dbname=mydb port=5432 sslmode=disable TimeZone=Asia/Shanghai"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
return db
}

// MySQL
func connectMySQL() *gorm.DB {
dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
return db
}

// SQLite
func connectSQLite() *gorm.DB {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
return db
}

Model Definition

package models

import (
"time"
"gorm.io/gorm"
)

// User model
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:255;not null"`
Email string `gorm:"uniqueIndex;not null"`
Age int `gorm:"default:0"`
Birthday *time.Time
Active bool `gorm:"default:true"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`

// Associations
Profile Profile `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
Orders []Order
CreditCard CreditCard `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
}

// Profile model
type Profile struct {
ID uint `gorm:"primaryKey"`
UserID uint `gorm:"uniqueIndex"`
Bio string `gorm:"type:text"`
Avatar string
Website string
}

// Order model
type Order struct {
ID uint `gorm:"primaryKey"`
UserID uint `gorm:"index"`
Amount float64 `gorm:"precision:10;scale:2"`
Status string `gorm:"size:50;default:'pending'"`
OrderDate time.Time

User User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
Items []Item `gorm:"many2many:order_items;"`
}

// Item model
type Item struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:255;not null"`
Description string `gorm:"type:text"`
Price float64 `gorm:"precision:10;scale:2;not null"`
Stock int `gorm:"default:0"`
CategoryID uint `gorm:"index"`

Category Category `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
Orders []Order `gorm:"many2many:order_items;"`
}

// Category model
type Category struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:100;uniqueIndex;not null"`
Items []Item
}

// CreditCard model (one-to-one with User)
type CreditCard struct {
ID uint `gorm:"primaryKey"`
UserID uint `gorm:"uniqueIndex"`
Number string `gorm:"size:20;not null"`
Expiry string `gorm:"size:5;not null"`
}

CRUD Operations

Create

package main

import (
"fmt"
"time"
"your-project/models"
)

func createOperations(db *gorm.DB) {
// Create single record
user := models.User{
Name: "John Doe",
Email: "john@example.com",
Age: 30,
Birthday: &time.Time{},
}

result := db.Create(&user)
if result.Error != nil {
fmt.Printf("Error creating user: %v\n", result.Error)
return
}
fmt.Printf("Created user with ID: %d\n", user.ID)

// Create multiple records
users := []models.User{
{Name: "Alice", Email: "alice@example.com", Age: 25},
{Name: "Bob", Email: "bob@example.com", Age: 28},
{Name: "Charlie", Email: "charlie@example.com", Age: 35},
}

db.Create(&users)

// Create with associated records
userWithProfile := models.User{
Name: "Jane Doe",
Email: "jane@example.com",
Age: 27,
Profile: models.Profile{
Bio: "Software Developer",
Website: "https://jane.dev",
},
CreditCard: models.CreditCard{
Number: "1234-5678-9012-3456",
Expiry: "12/25",
},
}

db.Create(&userWithProfile)
}

Read/Query

func queryOperations(db *gorm.DB) {
// Find by primary key
var user models.User
db.First(&user, 1) // Find user with ID 1

// Find by condition
db.Where("name = ?", "John Doe").First(&user)
db.Where("age > ?", 25).Find(&users)

// Multiple conditions
db.Where("name = ? AND age = ?", "John Doe", 30).First(&user)
db.Where("name IN ?", []string{"John", "Alice", "Bob"}).Find(&users)

// Advanced queries
var users []models.User

// Select specific fields
db.Select("name", "email").Where("age > ?", 25).Find(&users)

// Order by
db.Order("age desc").Find(&users)
db.Order("age desc, name").Find(&users)

// Limit and Offset
db.Limit(10).Offset(5).Find(&users)

// Group and Having
type Result struct {
Age int
Count int
}
var results []Result
db.Model(&models.User{}).Select("age, count(*) as count").Group("age").Having("count > ?", 1).Scan(&results)

// Joins
db.Joins("Profile").Where("users.age > ?", 25).Find(&users)

// Preloading associations
db.Preload("Profile").Preload("Orders").Find(&users)
db.Preload("Orders.Items").Find(&users)

// Custom preloading
db.Preload("Orders", "amount > ?", 100).Find(&users)
}

Update

func updateOperations(db *gorm.DB) {
var user models.User
db.First(&user, 1)

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

// Update multiple fields with struct
db.Model(&user).Updates(models.User{Name: "John Smith", Age: 31})

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

// Update selected fields
db.Model(&user).Select("name", "age").Updates(map[string]interface{}{"name": "John", "age": 33, "active": false})

// Batch updates
db.Model(&models.User{}).Where("age < ?", 25).Update("active", false)

// Update with SQL expression
db.Model(&user).Update("age", gorm.Expr("age + ?", 1))

// Update with returning (PostgreSQL)
db.Model(&user).Clauses(clause.Returning{}).Where("id = ?", user.ID).Update("name", "Updated Name")
}

Delete

func deleteOperations(db *gorm.DB) {
var user models.User

// Soft delete (if DeletedAt field exists)
db.Delete(&user, 1)

// Batch soft delete
db.Where("age < ?", 18).Delete(&models.User{})

// Permanent delete
db.Unscoped().Delete(&user, 1)

// Find soft deleted records
db.Unscoped().Where("deleted_at IS NOT NULL").Find(&users)

// Restore soft deleted records
db.Unscoped().Model(&user).Where("id = ?", 1).Update("deleted_at", nil)
}

Associations

Has One

// User has one Profile
func hasOneExample(db *gorm.DB) {
var user models.User

// Create user with profile
user = models.User{
Name: "John",
Email: "john@example.com",
Profile: models.Profile{
Bio: "Developer",
},
}
db.Create(&user)

// Load profile
db.Preload("Profile").First(&user, user.ID)

// Create profile for existing user
profile := models.Profile{
UserID: user.ID,
Bio: "Updated bio",
}
db.Create(&profile)
}

Has Many

// User has many Orders
func hasManyExample(db *gorm.DB) {
var user models.User
db.First(&user, 1)

// Create orders for user
orders := []models.Order{
{UserID: user.ID, Amount: 99.99, Status: "completed"},
{UserID: user.ID, Amount: 149.50, Status: "pending"},
}
db.Create(&orders)

// Load user with orders
db.Preload("Orders").First(&user, user.ID)

// Add orders to user
newOrders := []models.Order{
{Amount: 79.99, Status: "processing"},
}
db.Model(&user).Association("Orders").Append(&newOrders)

// Count orders
count := db.Model(&user).Association("Orders").Count()
fmt.Printf("User has %d orders\n", count)
}

Many to Many

// Order belongs to many Items, Item belongs to many Orders
func manyToManyExample(db *gorm.DB) {
// Create items
items := []models.Item{
{Name: "Laptop", Price: 999.99},
{Name: "Mouse", Price: 29.99},
{Name: "Keyboard", Price: 79.99},
}
db.Create(&items)

// Create order with items
order := models.Order{
UserID: 1,
Amount: 1109.97,
Status: "completed",
}
db.Create(&order)

// Associate items with order
db.Model(&order).Association("Items").Append([]models.Item{items[0], items[1], items[2]})

// Load order with items
db.Preload("Items").First(&order, order.ID)

// Remove association
db.Model(&order).Association("Items").Delete(&items[2])

// Replace associations
db.Model(&order).Association("Items").Replace([]models.Item{items[0]})
}

Advanced Features

Transactions

func transactionExample(db *gorm.DB) {
// Manual transaction
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()

if err := tx.Error; err != nil {
return
}

user := models.User{Name: "Transaction User", Email: "tx@example.com"}
if err := tx.Create(&user).Error; err != nil {
tx.Rollback()
return
}

profile := models.Profile{UserID: user.ID, Bio: "Transaction bio"}
if err := tx.Create(&profile).Error; err != nil {
tx.Rollback()
return
}

tx.Commit()

// Transaction with callback
db.Transaction(func(tx *gorm.DB) error {
user := models.User{Name: "Callback User", Email: "callback@example.com"}
if err := tx.Create(&user).Error; err != nil {
return err
}

profile := models.Profile{UserID: user.ID, Bio: "Callback bio"}
if err := tx.Create(&profile).Error; err != nil {
return err
}

return nil
})
}

Hooks

// Model with hooks
type UserWithHooks struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:255"`
Email string `gorm:"uniqueIndex"`
Password string `gorm:"size:255"`
CreatedAt time.Time
UpdatedAt time.Time
}

// BeforeCreate hook
func (u *UserWithHooks) BeforeCreate(tx *gorm.DB) (err error) {
// Hash password before creating
if u.Password != "" {
// hashedPassword := hashPassword(u.Password)
// u.Password = hashedPassword
}

// Validate email
if !strings.Contains(u.Email, "@") {
return errors.New("invalid email format")
}

return
}

// AfterCreate hook
func (u *UserWithHooks) AfterCreate(tx *gorm.DB) (err error) {
// Send welcome email
fmt.Printf("Welcome email sent to %s\n", u.Email)
return
}

// BeforeUpdate hook
func (u *UserWithHooks) BeforeUpdate(tx *gorm.DB) (err error) {
// Log update
fmt.Printf("Updating user %d\n", u.ID)
return
}

Scopes

// Define scopes
func ActiveUsers(db *gorm.DB) *gorm.DB {
return db.Where("active = ?", true)
}

func AdultUsers(db *gorm.DB) *gorm.DB {
return db.Where("age >= ?", 18)
}

func RecentUsers(db *gorm.DB) *gorm.DB {
return db.Where("created_at > ?", time.Now().AddDate(0, 0, -30))
}

// Use scopes
func scopeExample(db *gorm.DB) {
var users []models.User

// Single scope
db.Scopes(ActiveUsers).Find(&users)

// Multiple scopes
db.Scopes(ActiveUsers, AdultUsers, RecentUsers).Find(&users)

// Dynamic scopes
ageScope := func(minAge int) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Where("age >= ?", minAge)
}
}

db.Scopes(ageScope(25)).Find(&users)
}

Raw SQL

func rawSQLExample(db *gorm.DB) {
var users []models.User
var count int64

// Raw query
db.Raw("SELECT * FROM users WHERE age > ?", 25).Scan(&users)

// Raw query with struct
type Result struct {
Name string
Email string
Age int
}
var results []Result
db.Raw("SELECT name, email, age FROM users WHERE active = ?", true).Scan(&results)

// Execute raw SQL
db.Exec("UPDATE users SET active = ? WHERE age < ?", false, 18)

// Row query
row := db.Raw("SELECT count(*) FROM users WHERE active = ?", true).Row()
row.Scan(&count)

// Named arguments
db.Raw("SELECT * FROM users WHERE name = @name AND age > @age",
sql.Named("name", "John"),
sql.Named("age", 25)).Scan(&users)
}

Migrations

Auto Migration

func autoMigrate(db *gorm.DB) {
// Auto migrate tables
db.AutoMigrate(
&models.User{},
&models.Profile{},
&models.Order{},
&models.Item{},
&models.Category{},
&models.CreditCard{},
)
}

Manual Migrations

func manualMigrations(db *gorm.DB) {
// Create table
db.Migrator().CreateTable(&models.User{})

// Check if table exists
if db.Migrator().HasTable(&models.User{}) {
fmt.Println("User table exists")
}

// Add column
db.Migrator().AddColumn(&models.User{}, "middle_name")

// Check if column exists
if db.Migrator().HasColumn(&models.User{}, "middle_name") {
fmt.Println("middle_name column exists")
}

// Drop column
db.Migrator().DropColumn(&models.User{}, "middle_name")

// Add index
db.Migrator().CreateIndex(&models.User{}, "idx_user_email")

// Drop index
db.Migrator().DropIndex(&models.User{}, "idx_user_email")

// Rename table
db.Migrator().RenameTable(&models.User{}, "app_users")
db.Migrator().RenameTable("app_users", &models.User{})
}

Performance Optimization

Database Connection Pool

func configureConnectionPool(db *gorm.DB) {
sqlDB, err := db.DB()
if err != nil {
log.Fatal("Failed to get database instance:", err)
}

// Set maximum number of open connections
sqlDB.SetMaxOpenConns(100)

// Set maximum number of idle connections
sqlDB.SetMaxIdleConns(10)

// Set maximum lifetime of connections
sqlDB.SetConnMaxLifetime(time.Hour)
}

Batch Operations

func batchOperations(db *gorm.DB) {
// Batch insert
users := make([]models.User, 1000)
for i := range users {
users[i] = models.User{
Name: fmt.Sprintf("User%d", i),
Email: fmt.Sprintf("user%d@example.com", i),
Age: 20 + (i % 50),
}
}

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

// Find in batches
db.FindInBatches(&users, 100, func(tx *gorm.DB, batch int) error {
for _, user := range users {
// Process each user
fmt.Printf("Processing user: %s\n", user.Name)
}
return nil
})
}

Query Optimization

func queryOptimization(db *gorm.DB) {
var users []models.User

// Use indexes
db.Where("email = ?", "john@example.com").First(&users) // email should be indexed

// Select only needed fields
db.Select("id", "name", "email").Find(&users)

// Use joins instead of N+1 queries
db.Joins("Profile").Find(&users)

// Preload with conditions
db.Preload("Orders", "amount > ?", 100).Find(&users)

// Use exists for better performance
var exists bool
db.Model(&models.User{}).Select("count(*) > 0").Where("email = ?", "john@example.com").Find(&exists)

// Use Count for counting
var count int64
db.Model(&models.User{}).Where("active = ?", true).Count(&count)
}

Testing

Test Setup

package models_test

import (
"testing"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"your-project/models"
)

func setupTestDB() *gorm.DB {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
panic("Failed to connect to test database")
}

// Auto migrate test tables
db.AutoMigrate(&models.User{}, &models.Profile{})

return db
}

func TestUserCreation(t *testing.T) {
db := setupTestDB()

user := models.User{
Name: "Test User",
Email: "test@example.com",
Age: 25,
}

result := db.Create(&user)
if result.Error != nil {
t.Fatalf("Failed to create user: %v", result.Error)
}

if user.ID == 0 {
t.Fatal("User ID should not be zero after creation")
}

// Verify user was created
var foundUser models.User
db.First(&foundUser, user.ID)

if foundUser.Name != user.Name {
t.Errorf("Expected name %s, got %s", user.Name, foundUser.Name)
}
}

func TestUserQueries(t *testing.T) {
db := setupTestDB()

// Create test data
users := []models.User{
{Name: "Alice", Email: "alice@example.com", Age: 25},
{Name: "Bob", Email: "bob@example.com", Age: 30},
{Name: "Charlie", Email: "charlie@example.com", Age: 35},
}
db.Create(&users)

// Test queries
var result []models.User
db.Where("age > ?", 28).Find(&result)

if len(result) != 2 {
t.Errorf("Expected 2 users, got %d", len(result))
}
}

Best Practices

1. Model Design

// Good: Use proper tags and relationships
type User struct {
ID uint `gorm:"primaryKey"`
Email string `gorm:"uniqueIndex;not null;size:255"`
Name string `gorm:"not null;size:100"`
CreatedAt time.Time `gorm:"not null"`
UpdatedAt time.Time `gorm:"not null"`
}

// Bad: Missing constraints and proper sizing
type BadUser struct {
ID uint
Email string
Name string
}

2. Error Handling

func properErrorHandling(db *gorm.DB) {
var user models.User

// Always check for errors
if err := db.First(&user, 1).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// Handle not found case
fmt.Println("User not found")
} else {
// Handle other errors
log.Printf("Database error: %v", err)
}
return
}

// Use RowsAffected for updates/deletes
result := db.Model(&user).Update("name", "Updated Name")
if result.Error != nil {
log.Printf("Update error: %v", result.Error)
return
}

if result.RowsAffected == 0 {
fmt.Println("No rows were updated")
}
}

3. Repository Pattern

type UserRepository interface {
Create(user *models.User) error
GetByID(id uint) (*models.User, error)
GetByEmail(email string) (*models.User, error)
Update(user *models.User) error
Delete(id uint) error
List(limit, offset int) ([]models.User, error)
}

type userRepository struct {
db *gorm.DB
}

func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{db: db}
}

func (r *userRepository) Create(user *models.User) error {
return r.db.Create(user).Error
}

func (r *userRepository) GetByID(id uint) (*models.User, error) {
var user models.User
err := r.db.First(&user, id).Error
if err != nil {
return nil, err
}
return &user, nil
}

func (r *userRepository) GetByEmail(email string) (*models.User, error) {
var user models.User
err := r.db.Where("email = ?", email).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}

func (r *userRepository) Update(user *models.User) error {
return r.db.Save(user).Error
}

func (r *userRepository) Delete(id uint) error {
return r.db.Delete(&models.User{}, id).Error
}

func (r *userRepository) List(limit, offset int) ([]models.User, error) {
var users []models.User
err := r.db.Limit(limit).Offset(offset).Find(&users).Error
return users, err
}

This comprehensive GORM guide covers everything from basic setup to advanced features, providing practical examples and best practices for building robust database-driven Go applications.