Skip to main content

GORM — Mastering Go ORM

GORM is the most widely used ORM (Object-Relational Mapper) library in Go. You can manipulate databases using Go structs and methods without writing SQL directly.


GORM Introduction and Installation

go get gorm.io/gorm
go get gorm.io/driver/postgres # PostgreSQL
go get gorm.io/driver/mysql # MySQL
go get gorm.io/driver/sqlite # SQLite

DB Connection

package main

import (
"fmt"
"log"

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

func connectDB() (*gorm.DB, error) {
dsn := "host=localhost user=myuser password=mypass dbname=mydb port=5432 sslmode=disable"

db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info), // SQL query logging
})
if err != nil {
return nil, fmt.Errorf("DB connection failed: %w", err)
}

// Access internal *sql.DB (connection pool configuration)
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxOpenConns(25)
sqlDB.SetMaxIdleConns(25)

return db, nil
}

Model Definition

GORM models are defined as Go structs. Tags (gorm:"...") control column behavior.

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

// Embedding gorm.Model: automatically includes ID, CreatedAt, UpdatedAt, DeletedAt
type User struct {
gorm.Model
Name string `gorm:"size:100;not null"`
Email string `gorm:"size:150;uniqueIndex;not null"`
Age int `gorm:"check:age > 0"`
Role string `gorm:"default:member"`
// Associations
Orders []Order `gorm:"foreignKey:UserID"`
}

type Order struct {
gorm.Model
UserID uint `gorm:"not null;index"`
ProductID uint `gorm:"not null"`
Amount float64 `gorm:"not null"`
Status string `gorm:"default:pending"`
// Reverse references
User User `gorm:"foreignKey:UserID"`
Product Product `gorm:"foreignKey:ProductID"`
}

type Product struct {
gorm.Model
Name string `gorm:"size:200;not null"`
Price float64 `gorm:"not null"`
Stock int `gorm:"default:0"`
}

gorm.Model Definition (Internal Structure)

type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"` // soft delete support
}

AutoMigrate — Automatic Migration

func migrate(db *gorm.DB) error {
return db.AutoMigrate(
&User{},
&Product{},
&Order{},
)
}

AutoMigrate creates tables if they don't exist and adds new columns if present. It doesn't delete existing columns or change types, so for production use a separate migration tool (like goose).


CRUD Basic Operations

Create

func createUser(db *gorm.DB, name, email string, age int) (*User, error) {
user := &User{Name: name, Email: email, Age: age}

// Save all fields
result := db.Create(user)
if result.Error != nil {
return nil, fmt.Errorf("create user failed: %w", result.Error)
}
// user.ID is automatically set with the generated ID
fmt.Printf("Created user ID: %d\n", user.ID)
return user, nil
}

// Save specific fields only
func createUserWithFields(db *gorm.DB, user *User) error {
return db.Select("Name", "Email").Create(user).Error
}

// Batch create
func createUsersBatch(db *gorm.DB, users []User) error {
return db.CreateInBatches(users, 100).Error // batch by 100
}

Read

// Single query - by primary key
func getUserByID(db *gorm.DB, id uint) (*User, error) {
var user User
result := db.First(&user, id) // WHERE id = id ORDER BY id LIMIT 1
if result.Error == gorm.ErrRecordNotFound {
return nil, nil
}
return &user, result.Error
}

// Conditional query
func getUserByEmail(db *gorm.DB, email string) (*User, error) {
var user User
result := db.Where("email = ?", email).First(&user)
if result.Error == gorm.ErrRecordNotFound {
return nil, nil
}
return &user, result.Error
}

// Fetch all + sort + pagination
func getUsers(db *gorm.DB, page, limit int) ([]User, int64, error) {
var users []User
var total int64

db.Model(&User{}).Count(&total)

result := db.
Order("created_at DESC").
Offset((page - 1) * limit).
Limit(limit).
Find(&users)

return users, total, result.Error
}

// Query with struct conditions
func findUsers(db *gorm.DB, role string, minAge int) ([]User, error) {
var users []User
result := db.Where("role = ? AND age >= ?", role, minAge).Find(&users)
return users, result.Error
}

Update

// Save: update all fields (including zero values)
func saveUser(db *gorm.DB, user *User) error {
return db.Save(user).Error
}

// Updates: update changed fields only
func updateUserName(db *gorm.DB, id uint, name string) error {
return db.Model(&User{}).Where("id = ?", id).
Updates(map[string]interface{}{"name": name}).Error
}

// Update with struct (zero value fields excluded)
func updateUser(db *gorm.DB, id uint, updates User) error {
return db.Model(&User{}).Where("id = ?", id).Updates(updates).Error
}

// Update + force specific fields
func updateUserAge(db *gorm.DB, id uint, age int) error {
return db.Model(&User{}).Where("id = ?", id).
Select("Age").
Updates(User{Age: age}).Error
}

Delete

// Soft Delete (auto-applied if DeletedAt field exists)
func deleteUser(db *gorm.DB, id uint) error {
return db.Delete(&User{}, id).Error
}

// Hard Delete (permanent deletion)
func hardDeleteUser(db *gorm.DB, id uint) error {
return db.Unscoped().Delete(&User{}, id).Error
}

// Conditional delete
func deleteOldOrders(db *gorm.DB, days int) error {
return db.Where("created_at < NOW() - INTERVAL '? days'", days).
Delete(&Order{}).Error
}

Associations

// Prevent N+1: Preload all at once
func getUserWithOrders(db *gorm.DB, id uint) (*User, error) {
var user User
result := db.Preload("Orders").Preload("Orders.Product").First(&user, id)
return &user, result.Error
}

// Conditional Preload
func getUserWithPendingOrders(db *gorm.DB, id uint) (*User, error) {
var user User
result := db.Preload("Orders", "status = ?", "pending").First(&user, id)
return &user, result.Error
}

// Nested Preload (Joins approach)
func getUsersWithOrders(db *gorm.DB) ([]User, error) {
var users []User
result := db.
Joins("LEFT JOIN orders ON orders.user_id = users.id AND orders.deleted_at IS NULL").
Preload("Orders").
Find(&users)
return users, result.Error
}

Association Manipulation

// Add associated data
func addOrderToUser(db *gorm.DB, userID uint, order *Order) error {
var user User
if err := db.First(&user, userID).Error; err != nil {
return err
}
return db.Model(&user).Association("Orders").Append(order)
}

// Count associated data
func countUserOrders(db *gorm.DB, userID uint) int64 {
var user User
db.First(&user, userID)
return db.Model(&user).Association("Orders").Count()
}

Transactions

func createOrderWithStock(db *gorm.DB, userID, productID uint, amount float64) error {
return db.Transaction(func(tx *gorm.DB) error {
// Check stock
var product Product
if err := tx.First(&product, productID).Error; err != nil {
return err
}
if product.Stock <= 0 {
return fmt.Errorf("out of stock")
}

// Create order
order := Order{
UserID: userID,
ProductID: productID,
Amount: amount,
Status: "confirmed",
}
if err := tx.Create(&order).Error; err != nil {
return err
}

// Deduct stock
if err := tx.Model(&product).Update("stock", product.Stock-1).Error; err != nil {
return err
}

return nil // nil return triggers auto Commit
// error return triggers auto Rollback
})
}

Raw SQL & Named Queries

Use Raw SQL directly when GORM-generated queries don't suit your needs.

// Query with raw SQL
func getUsersByAgeRange(db *gorm.DB, min, max int) ([]User, error) {
var users []User
result := db.Raw("SELECT * FROM users WHERE age BETWEEN ? AND ?", min, max).Scan(&users)
return users, result.Error
}

// Exec: raw SQL with no result
func truncateTable(db *gorm.DB, tableName string) error {
// Warning: table names cannot be parameter-bound → validate first
return db.Exec("TRUNCATE TABLE " + tableName).Error
}

// Named parameters
func findUserByNamedParam(db *gorm.DB, name, email string) (*User, error) {
var user User
result := db.Where("name = @name OR email = @email",
sql.Named("name", name),
sql.Named("email", email),
).First(&user)
return &user, result.Error
}

Hooks — Lifecycle Hooks

type User struct {
gorm.Model
Name string
Email string
Password string `gorm:"->"` // read-only (excluded on save)
Hash string
}

// BeforeCreate: executed before create
func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.Password != "" {
hash, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Hash = string(hash)
u.Password = "" // don't store original password
}
return nil
}

// AfterFind: executed after query
func (u *User) AfterFind(tx *gorm.DB) error {
// post-process if needed
return nil
}

Key Summary

MethodDescription
db.Create(&model)Create record
db.First(&model, id)Single query by primary key
db.Find(&models)Multiple query
db.Save(&model)Save/update all fields
db.Updates(...)Update changed fields only
db.Delete(&model, id)Delete (Soft Delete)
db.Preload("Field")Eager load associated data
db.Transaction(fn)Execute transaction

GORM is advantageous for fast development, but for complex queries or performance-critical sections, consider Raw SQL or sqlc.