GORM: Object-Relational Mapping in Go
Introduction
GORM is the most popular ORM library for Go. It provides a clean, intuitive API for database operations while maintaining type safety. This guide covers models, queries, associations, and practical patterns for building database-driven applications.
Core Concepts
What is GORM?
GORM is an ORM that:
- Maps database tables to Go structs
- Provides chainable query API
- Supports associations and relationships
- Handles migrations automatically
- Works with multiple databases
Key Features
- Models: Struct-based database schema
- Queries: Chainable query builder
- Associations: Relationships between models
- Hooks: Lifecycle callbacks
- Migrations: Schema management
Good: Defining Models
Basic Models
package main
import (
"gorm.io/gorm"
"time"
)
// โ
GOOD: Define GORM model
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"index"`
Email string `gorm:"uniqueIndex"`
Age int
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
// โ
GOOD: Model with associations
type Post struct {
ID uint
Title string
Content string
UserID uint
User User `gorm:"foreignKey:UserID"`
CreatedAt time.Time
UpdatedAt time.Time
}
// โ
GOOD: Model with many-to-many
type Tag struct {
ID uint
Name string
Posts []Post `gorm:"many2many:post_tags;"`
}
// โ
GOOD: Custom table name
func (User) TableName() string {
return "users"
}
// โ
GOOD: Model with hooks
func (u *User) BeforeSave(tx *gorm.DB) error {
// Validate before saving
if u.Email == "" {
return gorm.ErrInvalidData
}
return nil
}
func (u *User) AfterCreate(tx *gorm.DB) error {
// Send welcome email
return nil
}
Good: Database Operations
CRUD Operations
package main
import (
"gorm.io/gorm"
)
// โ
GOOD: Create
func createUser(db *gorm.DB, user *User) error {
return db.Create(user).Error
}
// โ
GOOD: Read
func getUser(db *gorm.DB, id uint) (*User, error) {
var user User
err := db.First(&user, id).Error
return &user, err
}
// โ
GOOD: Update
func updateUser(db *gorm.DB, user *User) error {
return db.Save(user).Error
}
// โ
GOOD: Delete
func deleteUser(db *gorm.DB, id uint) error {
return db.Delete(&User{}, id).Error
}
// โ
GOOD: Batch operations
func createUsers(db *gorm.DB, users []User) error {
return db.CreateInBatches(users, 100).Error
}
// โ
GOOD: Soft delete
func softDeleteUser(db *gorm.DB, id uint) error {
return db.Delete(&User{}, id).Error
}
// โ
GOOD: Restore soft deleted
func restoreUser(db *gorm.DB, id uint) error {
return db.Model(&User{}).Where("id = ?", id).Update("deleted_at", nil).Error
}
Query Operations
package main
import (
"gorm.io/gorm"
)
// โ
GOOD: Find all
func getAllUsers(db *gorm.DB) ([]User, error) {
var users []User
err := db.Find(&users).Error
return users, err
}
// โ
GOOD: Find with conditions
func findUsersByAge(db *gorm.DB, minAge int) ([]User, error) {
var users []User
err := db.Where("age > ?", minAge).Find(&users).Error
return users, err
}
// โ
GOOD: Find with multiple conditions
func findUsers(db *gorm.DB, name string, minAge int) ([]User, error) {
var users []User
err := db.Where("name LIKE ? AND age > ?", "%"+name+"%", minAge).Find(&users).Error
return users, err
}
// โ
GOOD: Pagination
func getPaginatedUsers(db *gorm.DB, page, pageSize int) ([]User, error) {
var users []User
offset := (page - 1) * pageSize
err := db.Offset(offset).Limit(pageSize).Find(&users).Error
return users, err
}
// โ
GOOD: Sorting
func getSortedUsers(db *gorm.DB) ([]User, error) {
var users []User
err := db.Order("created_at DESC").Find(&users).Error
return users, err
}
// โ
GOOD: Select specific columns
func getUserEmails(db *gorm.DB) ([]string, error) {
var emails []string
err := db.Model(&User{}).Pluck("email", &emails).Error
return emails, err
}
// โ
GOOD: Count
func countUsers(db *gorm.DB) (int64, error) {
var count int64
err := db.Model(&User{}).Count(&count).Error
return count, err
}
Good: Associations
Working with Relationships
package main
import (
"gorm.io/gorm"
)
// โ
GOOD: Create with association
func createUserWithPosts(db *gorm.DB, user *User, posts []Post) error {
return db.Create(&User{
Name: user.Name,
Email: user.Email,
Posts: posts,
}).Error
}
// โ
GOOD: Load associations
func getUserWithPosts(db *gorm.DB, id uint) (*User, error) {
var user User
err := db.Preload("Posts").First(&user, id).Error
return &user, err
}
// โ
GOOD: Load nested associations
func getUserWithPostsAndTags(db *gorm.DB, id uint) (*User, error) {
var user User
err := db.Preload("Posts.Tags").First(&user, id).Error
return &user, err
}
// โ
GOOD: Conditional preload
func getUserWithRecentPosts(db *gorm.DB, id uint) (*User, error) {
var user User
err := db.Preload("Posts", "created_at > ?", time.Now().AddDate(0, -1, 0)).
First(&user, id).Error
return &user, err
}
// โ
GOOD: Update association
func updateUserPosts(db *gorm.DB, userID uint, posts []Post) error {
return db.Model(&User{}).Where("id = ?", userID).Association("Posts").Replace(posts)
}
// โ
GOOD: Add to association
func addPostToUser(db *gorm.DB, userID uint, post *Post) error {
return db.Model(&User{}).Where("id = ?", userID).Association("Posts").Append(post)
}
Advanced Patterns
Transactions
package main
import (
"gorm.io/gorm"
)
// โ
GOOD: Transaction handling
func transferFunds(db *gorm.DB, fromID, toID uint, amount float64) error {
return db.Transaction(func(tx *gorm.DB) error {
// Deduct from source
if err := tx.Model(&Account{}).Where("id = ?", fromID).
Update("balance", gorm.Expr("balance - ?", amount)).Error; err != nil {
return err
}
// Add to destination
if err := tx.Model(&Account{}).Where("id = ?", toID).
Update("balance", gorm.Expr("balance + ?", amount)).Error; err != nil {
return err
}
return nil
})
}
Scopes
package main
import (
"gorm.io/gorm"
)
// โ
GOOD: Define scopes
func activeUsers(db *gorm.DB) *gorm.DB {
return db.Where("deleted_at IS NULL")
}
func recentUsers(db *gorm.DB) *gorm.DB {
return db.Where("created_at > ?", time.Now().AddDate(0, -1, 0))
}
// โ
GOOD: Use scopes
func getActiveRecentUsers(db *gorm.DB) ([]User, error) {
var users []User
err := db.Scopes(activeUsers, recentUsers).Find(&users).Error
return users, err
}
Best Practices
1. Use Prepared Statements
// โ
GOOD: Parameterized queries
db.Where("email = ?", email).First(&user)
// โ BAD: String concatenation
db.Where("email = '" + email + "'").First(&user)
2. Preload Associations
// โ
GOOD: Preload to avoid N+1 queries
db.Preload("Posts").Find(&users)
// โ BAD: N+1 query problem
for _, user := range users {
db.Find(&user.Posts)
}
3. Use Transactions
// โ
GOOD: Wrap related operations
db.Transaction(func(tx *gorm.DB) error {
// Multiple operations
return nil
})
// โ BAD: No transaction
db.Create(&user)
db.Create(&post)
Resources
- GORM Documentation: https://gorm.io/
- GORM GitHub: https://github.com/go-gorm/gorm
- Database Drivers: https://gorm.io/docs/connecting_to_the_database.html
Summary
GORM is a powerful ORM for Go that simplifies database operations. Use models for schema definition, chainable queries for flexibility, and associations for relationships. Always use parameterized queries, preload associations to avoid N+1 queries, and wrap related operations in transactions.
Comments