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{},
)
}
AutoMigratecreates 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
Preload — Eager Load Related Data
// 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
| Method | Description |
|---|---|
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.