GORM is Go’s most popular Object-Relational Mapping (ORM) library, providing an elegant way to work with databases using Go structs. Instead of writing raw SQL, GORM lets you interact with database records as Go objects, handling the translation between your code and the database automatically.
Why Use an ORM? The fundamental challenge: bridging two worlds.
flowchart LR
subgraph GoWorld["Go World"]
S[Struct]
M[Methods]
T[Types]
end
subgraph DBWorld["Database World"]
TB[Tables]
R[Rows]
C[Columns]
end
GoWorld <-->|ORM| DBWorld
style GoWorld fill:#e3f2fd
style DBWorld fill:#e8f5e9
Without an ORM, you’d write the same data definition twice:
1 2 3 4 5 6 7 8 9 10 type Product struct { ID int Name string Description string Price float64 }
GORM benefits:
Define data once in Go structs
Type safety at compile time
Automatic relationship management
Built-in query builder
Migrations and schema management
Getting Started with GORM Installation 1 2 go get -u gorm.io/gorm go get -u gorm.io/driver/postgres
Connecting to PostgreSQL 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package mainimport ( "log" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" ) func main () { dsn := "host=localhost user=appuser password=secret dbname=myapp port=5432 sslmode=disable" 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) } sqlDB, err := db.DB() if err != nil { log.Fatal(err) } sqlDB.SetMaxIdleConns(10 ) sqlDB.SetMaxOpenConns(100 ) }
GORM Models Basic Model Definition GORM uses struct tags to configure database mapping:
1 2 3 4 5 6 7 8 9 10 type User struct { ID uint `gorm:"primaryKey"` Username string `gorm:"size:50;uniqueIndex;not null"` Email string `gorm:"size:255;uniqueIndex;not null"` Password string `gorm:"not null" json:"-"` IsActive bool `gorm:"default:true"` CreatedAt time.Time UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` }
Embedding gorm.Model GORM provides a base model with common fields:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Product struct { gorm.Model Name string `gorm:"size:255;not null"` Description string `gorm:"type:text"` Price float64 `gorm:"not null;check:price > 0"` Stock int `gorm:"default:0;check:stock >= 0"` CategoryID uint }
Tag
Description
Example
primaryKey
Mark as primary key
gorm:"primaryKey"
size
Column size
gorm:"size:255"
type
Column type
gorm:"type:text"
uniqueIndex
Unique index
gorm:"uniqueIndex"
not null
Not nullable
gorm:"not null"
default
Default value
gorm:"default:0"
check
Check constraint
gorm:"check:price > 0"
index
Create index
gorm:"index"
column
Custom column name
gorm:"column:user_name"
CRUD Operations Create 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 user := User{Username: "john" , Email: "[email protected] " , Password: "hashed" } result := db.Create(&user) if result.Error != nil { log.Error("Failed to create user:" , result.Error) } fmt.Printf("Created user with ID: %d\n" , user.ID) users := []User{ {Username: "alice" , Email: "[email protected] " }, {Username: "bob" , Email: "[email protected] " }, } db.Create(&users) db.Select("Username" , "Email" ).Create(&user) db.Omit("Password" ).Create(&user)
Read 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 var user Userdb.First(&user, 1 ) db.First(&user, "id = ?" , "1" ) db.First(&user, "username = ?" , "john" ) var users []Userdb.Find(&users) db.Where("is_active = ?" , true ).Find(&users) db.Where("username LIKE ?" , "%john%" ).Find(&users) db.Where("created_at > ?" , time.Now().AddDate(0 , -1 , 0 )).Find(&users) db.Where("is_active = ?" , true ). Where("created_at > ?" , lastWeek). Find(&users) db.Where("username = ?" , "john" ). Or("email = ?" , "[email protected] " ). First(&user) db.Select("username" , "email" ).Find(&users) db.Order("created_at desc" ).Limit(10 ).Offset(0 ).Find(&users)
Update 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 db.Model(&user).Update("username" , "johnny" ) db.Model(&user).Updates(User{Username: "johnny" , Email: "[email protected] " }) db.Model(&user).Updates(map [string ]interface {}{ "username" : "johnny" , "is_active" : false , }) db.Model(&User{}).Where("is_active = ?" , false ).Update("is_active" , true ) db.Model(&User{}).Where("created_at < ?" , lastYear).Update("is_active" , false )
Delete 1 2 3 4 5 6 7 8 9 10 11 12 13 14 db.Delete(&user, 1 ) db.Where("is_active = ?" , false ).Delete(&User{}) db.Delete(&user) db.Unscoped().Delete(&user) db.Unscoped().Where("deleted_at IS NOT NULL" ).Find(&users)
Relationships One-to-Many 1 2 3 4 5 6 7 8 9 10 11 12 13 type Category struct { gorm.Model Name string `gorm:"size:100;not null"` Products []Product `gorm:"foreignKey:CategoryID"` } type Product struct { gorm.Model Name string `gorm:"size:255;not null"` Price float64 CategoryID uint Category Category `gorm:"foreignKey:CategoryID"` }
erDiagram
CATEGORY ||--o{ PRODUCT : has
CATEGORY {
uint id PK
string name
}
PRODUCT {
uint id PK
string name
float price
uint category_id FK
}
Many-to-Many 1 2 3 4 5 6 7 8 9 10 11 type Product struct { gorm.Model Name string Tags []Tag `gorm:"many2many:product_tags;"` } type Tag struct { gorm.Model Name string Products []Product `gorm:"many2many:product_tags;"` }
Working with Relationships 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 category := Category{Name: "Electronics" } product := Product{Name: "Laptop" , Price: 999.99 , Category: category} db.Create(&product) var products []Productdb.Preload("Category" ).Find(&products) db.Preload("Category" ).Preload("Tags" ).Find(&products) db.Preload("Tags" , "name LIKE ?" , "%tech%" ).Find(&products) db.Model(&product).Association("Tags" ).Append(&Tag{Name: "Sale" }) db.Model(&product).Association("Tags" ).Delete(&tag) db.Model(&product).Association("Tags" ).Replace(&newTags) count := db.Model(&product).Association("Tags" ).Count()
GORM Hooks Hooks allow you to execute code before/after CRUD operations:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 type User struct { gorm.Model Username string Email string PasswordHash string } func (u *User) BeforeCreate(tx *gorm.DB) error { if u.PasswordHash != "" { hash, err := bcrypt.GenerateFromPassword([]byte (u.PasswordHash), bcrypt.DefaultCost) if err != nil { return err } u.PasswordHash = string (hash) } return nil } func (u *User) AfterCreate(tx *gorm.DB) error { log.Printf("New user created: ID=%d, Username=%s" , u.ID, u.Username) return nil } func (u *User) BeforeUpdate(tx *gorm.DB) error { if !isValidEmail(u.Email) { return errors.New("invalid email format" ) } return nil } func (u *User) AfterFind(tx *gorm.DB) error { u.PasswordHash = "" return nil }
flowchart TD
subgraph Create["Create Operation"]
BC[BeforeCreate] --> DB1[Database Insert]
DB1 --> AC[AfterCreate]
end
subgraph Read["Read Operation"]
DB2[Database Select] --> AF[AfterFind]
end
subgraph Update["Update Operation"]
BU[BeforeUpdate] --> DB3[Database Update]
DB3 --> AU[AfterUpdate]
end
style Create fill:#e8f5e9
style Read fill:#e3f2fd
style Update fill:#fff3e0
Advanced Queries Raw SQL 1 2 3 4 5 6 var users []Userdb.Raw("SELECT * FROM users WHERE is_active = ?" , true ).Scan(&users) db.Exec("UPDATE users SET is_active = ? WHERE last_login < ?" , false , lastYear)
Scopes Reusable query conditions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func Active (db *gorm.DB) *gorm.DB { return db.Where("is_active = ?" , true ) } func Recent (days int ) func (db *gorm.DB) *gorm.DB { return func (db *gorm.DB) *gorm.DB { return db.Where("created_at > ?" , time.Now().AddDate(0 , 0 , -days)) } } func Paginate (page, pageSize int ) func (db *gorm.DB) *gorm.DB { return func (db *gorm.DB) *gorm.DB { offset := (page - 1 ) * pageSize return db.Offset(offset).Limit(pageSize) } } db.Scopes(Active, Recent(7 ), Paginate(1 , 10 )).Find(&users)
Joins 1 2 3 4 5 6 7 8 9 10 11 12 type Result struct { ProductName string CategoryName string Price float64 } var results []Resultdb.Model(&Product{}). Select("products.name as product_name, categories.name as category_name, products.price" ). Joins("LEFT JOIN categories ON products.category_id = categories.id" ). Where("products.price > ?" , 100 ). Scan(&results)
Subqueries 1 2 3 4 5 6 7 avgPrice := db.Model(&Product{}).Select("AVG(price)" ) db.Where("price > (?)" , avgPrice).Find(&products) usersWithOrders := db.Model(&Order{}).Select("user_id" ) db.Where("id IN (?)" , usersWithOrders).Find(&users)
Transactions 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func TransferMoney (db *gorm.DB, fromID, toID uint , amount float64 ) error { return db.Transaction(func (tx *gorm.DB) error { if err := tx.Model(&Account{}). Where("id = ? AND balance >= ?" , fromID, amount). Update("balance" , gorm.Expr("balance - ?" , amount)).Error; err != nil { return err } if err := tx.Model(&Account{}). Where("id = ?" , toID). Update("balance" , gorm.Expr("balance + ?" , amount)).Error; err != nil { return err } record := Transaction{FromID: fromID, ToID: toID, Amount: amount} if err := tx.Create(&record).Error; err != nil { return err } return nil }) }
Auto-Migration 1 2 3 4 5 6 7 8 db.AutoMigrate(&User{}, &Product{}, &Category{}, &Order{})
Error Handling 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func GetUser (db *gorm.DB, id uint ) (*User, error ) { var user User result := db.First(&user, id) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil , ErrUserNotFound } return nil , result.Error } return &user, nil } result := db.Model(&User{}).Where("id = ?" , id).Update("is_active" , false ) if result.RowsAffected == 0 { return ErrUserNotFound }
Best Practices Repository Pattern 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 type UserRepository interface { Create(user *User) error FindByID(id uint ) (*User, error ) FindByEmail(email string ) (*User, error ) Update(user *User) error Delete(id uint ) error List(page, pageSize int ) ([]User, int64 , error ) } type userRepository struct { db *gorm.DB } func NewUserRepository (db *gorm.DB) UserRepository { return &userRepository{db: db} } func (r *userRepository) FindByID(id uint ) (*User, error ) { var user User if err := r.db.First(&user, id).Error; err != nil { return nil , err } return &user, nil } func (r *userRepository) List(page, pageSize int ) ([]User, int64 , error ) { var users []User var total int64 r.db.Model(&User{}).Count(&total) err := r.db.Scopes(Paginate(page, pageSize)).Find(&users).Error return users, total, err }
Summary
Feature
Key Points
Models
Define schema with struct tags
CRUD
Create, First, Find, Update, Delete
Relationships
BelongsTo, HasMany, ManyToMany with Preload
Hooks
BeforeCreate, AfterCreate, BeforeUpdate, etc.
Scopes
Reusable query conditions
Transactions
db.Transaction for atomic operations
Next post: Database Migrations in Go - Managing schema evolution safely with migration tools.
Comments