Skip to main content
โšก Calmops

GORM: Object-Relational Mapping in Go

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

  1. Models: Struct-based database schema
  2. Queries: Chainable query builder
  3. Associations: Relationships between models
  4. Hooks: Lifecycle callbacks
  5. 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

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