GORM Development Guide

GORM is the most popular ORM library for Go, providing a developer-friendly interface for database operations while maintaining excellent performance. This guide demonstrates how to leverage GORM's powerful features with Tacnode's PostgreSQL compatibility to build robust, scalable Go applications.

Prerequisites

Before starting, ensure you have:

  • Go 1.18+ (Go 1.20+ recommended for generics support)
  • Go modules initialized in your project
  • A Tacnode database with appropriate access credentials

Database Setup

Create your database structure in Tacnode:

-- Create the database
CREATE DATABASE example;
 
-- Connect to the database and create tables
\c example;
 
CREATE TABLE customers (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    phone VARCHAR(20),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    is_active BOOLEAN DEFAULT TRUE
);
 
-- Create indexes for better performance
CREATE INDEX idx_customers_email ON customers(email);
CREATE INDEX idx_customers_active ON customers(is_active);
CREATE INDEX idx_customers_created_at ON customers(created_at);

Project Setup

Go Module Initialization

Initialize your Go project and install dependencies:

# Initialize Go module
go mod init tacnode-gorm-example
 
# Install GORM and PostgreSQL driver
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
 
# Optional: Additional utilities
go get -u github.com/joho/godotenv  # Environment variables
go get -u github.com/google/uuid    # UUID support

Project Structure

Organize your project with a clean architecture:

tacnode-gorm-example/
├── cmd/
│   └── main.go
├── internal/
│   ├── config/
│   │   └── database.go
│   ├── models/
│   │   └── customer.go
│   ├── repository/
│   │   └── customer_repository.go
│   └── service/
│       └── customer_service.go
├── .env
├── go.mod
└── go.sum

Model Definition

Modern GORM Model with Embedded Fields

// internal/models/customer.go
package models
 
import (
    "time"
    "gorm.io/gorm"
    "github.com/google/uuid"
)
 
// BaseModel contains common fields for all models
type BaseModel struct {
    ID        uint           `gorm:"primarykey" json:"id"`
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
}
 
// Customer represents the customer entity
type Customer struct {
    BaseModel
    
    // Required fields
    Name  string `gorm:"type:varchar(255);not null" json:"name" validate:"required,min=2,max=255"`
    Email string `gorm:"type:varchar(255);uniqueIndex;not null" json:"email" validate:"required,email"`
    
    // Optional fields
    Phone    *string `gorm:"type:varchar(20)" json:"phone,omitempty"`
    IsActive bool    `gorm:"default:true;not null" json:"is_active"`
    
    // Additional metadata
    ExternalID uuid.UUID `gorm:"type:uuid;uniqueIndex" json:"external_id"`
    Metadata   string    `gorm:"type:jsonb" json:"metadata,omitempty"`
}
 
// TableName specifies the table name for the Customer model
func (Customer) TableName() string {
    return "customers"
}
 
// BeforeCreate is a GORM hook that runs before creating a record
func (c *Customer) BeforeCreate(tx *gorm.DB) error {
    if c.ExternalID == uuid.Nil {
        c.ExternalID = uuid.New()
    }
    return nil
}
 
// CustomerCreateRequest represents the request payload for creating a customer
type CustomerCreateRequest struct {
    Name  string  `json:"name" validate:"required,min=2,max=255"`
    Email string  `json:"email" validate:"required,email"`
    Phone *string `json:"phone,omitempty"`
}
 
// CustomerUpdateRequest represents the request payload for updating a customer
type CustomerUpdateRequest struct {
    Name     *string `json:"name,omitempty" validate:"omitempty,min=2,max=255"`
    Email    *string `json:"email,omitempty" validate:"omitempty,email"`
    Phone    *string `json:"phone,omitempty"`
    IsActive *bool   `json:"is_active,omitempty"`
}
 
// ToCustomer converts CreateRequest to Customer model
func (req *CustomerCreateRequest) ToCustomer() *Customer {
    return &Customer{
        Name:     req.Name,
        Email:    req.Email,
        Phone:    req.Phone,
        IsActive: true,
    }
}

Database Configuration

Connection Management with Configuration

// internal/config/database.go
package config
 
import (
    "fmt"
    "log"
    "os"
    "strconv"
    "time"
 
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)
 
// DatabaseConfig holds database configuration
type DatabaseConfig struct {
    Host         string
    Port         int
    User         string
    Password     string
    DBName       string
    SSLMode      string
    MaxOpenConns int
    MaxIdleConns int
    MaxIdleTime  time.Duration
}
 
// LoadDatabaseConfig loads configuration from environment variables
func LoadDatabaseConfig() *DatabaseConfig {
    port, _ := strconv.Atoi(getEnv("DB_PORT", "5432"))
    maxOpenConns, _ := strconv.Atoi(getEnv("DB_MAX_OPEN_CONNS", "25"))
    maxIdleConns, _ := strconv.Atoi(getEnv("DB_MAX_IDLE_CONNS", "5"))
    maxIdleTime, _ := time.ParseDuration(getEnv("DB_MAX_IDLE_TIME", "15m"))
 
    return &DatabaseConfig{
        Host:         getEnv("DB_HOST", "localhost"),
        Port:         port,
        User:         getEnv("DB_USER", "postgres"),
        Password:     getEnv("DB_PASSWORD", ""),
        DBName:       getEnv("DB_NAME", "example"),
        SSLMode:      getEnv("DB_SSL_MODE", "prefer"),
        MaxOpenConns: maxOpenConns,
        MaxIdleConns: maxIdleConns,
        MaxIdleTime:  maxIdleTime,
    }
}
 
// DSN generates the database connection string
func (cfg *DatabaseConfig) DSN() string {
    return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s TimeZone=UTC",
        cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName, cfg.SSLMode)
}
 
// ConnectDatabase establishes database connection with optimized settings
func ConnectDatabase(cfg *DatabaseConfig) (*gorm.DB, error) {
    // Configure GORM logger
    gormLogger := logger.Default
    if getEnv("DB_LOG_LEVEL", "silent") == "info" {
        gormLogger = logger.Default.LogMode(logger.Info)
    }
 
    // Connect to database
    db, err := gorm.Open(postgres.Open(cfg.DSN()), &gorm.Config{
        Logger:                 gormLogger,
        SkipDefaultTransaction: true, // Improve performance
        PrepareStmt:           true, // Cache prepared statements
    })
    if err != nil {
        return nil, fmt.Errorf("failed to connect to database: %w", err)
    }
 
    // Configure connection pool
    sqlDB, err := db.DB()
    if err != nil {
        return nil, fmt.Errorf("failed to get underlying sql.DB: %w", err)
    }
 
    sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
    sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
    sqlDB.SetConnMaxIdleTime(cfg.MaxIdleTime)
 
    return db, nil
}
 
// getEnv gets environment variable with default value
func getEnv(key, defaultValue string) string {
    if value := os.Getenv(key); value != "" {
        return value
    }
    return defaultValue
}

Repository Layer

Comprehensive Repository Implementation

// internal/repository/customer_repository.go
package repository
 
import (
    "errors"
    "fmt"
 
    "gorm.io/gorm"
    "tacnode-gorm-example/internal/models"
)
 
// CustomerRepository handles customer data access
type CustomerRepository struct {
    db *gorm.DB
}
 
// NewCustomerRepository creates a new customer repository
func NewCustomerRepository(db *gorm.DB) *CustomerRepository {
    return &CustomerRepository{db: db}
}
 
// Create creates a new customer
func (r *CustomerRepository) Create(customer *models.Customer) error {
    if err := r.db.Create(customer).Error; err != nil {
        return fmt.Errorf("failed to create customer: %w", err)
    }
    return nil
}
 
// GetByID retrieves a customer by ID
func (r *CustomerRepository) GetByID(id uint) (*models.Customer, error) {
    var customer models.Customer
    err := r.db.First(&customer, id).Error
    if err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, fmt.Errorf("customer with ID %d not found", id)
        }
        return nil, fmt.Errorf("failed to get customer: %w", err)
    }
    return &customer, nil
}
 
// GetByEmail retrieves a customer by email
func (r *CustomerRepository) GetByEmail(email string) (*models.Customer, error) {
    var customer models.Customer
    err := r.db.Where("email = ?", email).First(&customer).Error
    if err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, fmt.Errorf("customer with email %s not found", email)
        }
        return nil, fmt.Errorf("failed to get customer by email: %w", err)
    }
    return &customer, nil
}
 
// GetAll retrieves all customers with pagination and filtering
func (r *CustomerRepository) GetAll(limit, offset int, activeOnly bool) ([]models.Customer, int64, error) {
    var customers []models.Customer
    var total int64
 
    query := r.db.Model(&models.Customer{})
    if activeOnly {
        query = query.Where("is_active = ?", true)
    }
 
    // Get total count
    if err := query.Count(&total).Error; err != nil {
        return nil, 0, fmt.Errorf("failed to count customers: %w", err)
    }
 
    // Get paginated results
    if err := query.Limit(limit).Offset(offset).Find(&customers).Error; err != nil {
        return nil, 0, fmt.Errorf("failed to get customers: %w", err)
    }
 
    return customers, total, nil
}
 
// Search searches customers by name or email pattern
func (r *CustomerRepository) Search(pattern string, limit, offset int) ([]models.Customer, int64, error) {
    var customers []models.Customer
    var total int64
 
    searchPattern := "%" + pattern + "%"
    query := r.db.Model(&models.Customer{}).Where(
        "name ILIKE ? OR email ILIKE ?", searchPattern, searchPattern,
    )
 
    // Get total count
    if err := query.Count(&total).Error; err != nil {
        return nil, 0, fmt.Errorf("failed to count search results: %w", err)
    }
 
    // Get paginated results
    if err := query.Limit(limit).Offset(offset).Find(&customers).Error; err != nil {
        return nil, 0, fmt.Errorf("failed to search customers: %w", err)
    }
 
    return customers, total, nil
}
 
// Update updates a customer
func (r *CustomerRepository) Update(id uint, updates *models.CustomerUpdateRequest) (*models.Customer, error) {
    var customer models.Customer
    
    // Start transaction
    tx := r.db.Begin()
    defer func() {
        if r := recover(); r != nil {
            tx.Rollback()
        }
    }()
 
    // Get existing customer
    if err := tx.First(&customer, id).Error; err != nil {
        tx.Rollback()
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, fmt.Errorf("customer with ID %d not found", id)
        }
        return nil, fmt.Errorf("failed to get customer for update: %w", err)
    }
 
    // Apply updates
    updateData := make(map[string]interface{})
    if updates.Name != nil {
        updateData["name"] = *updates.Name
    }
    if updates.Email != nil {
        updateData["email"] = *updates.Email
    }
    if updates.Phone != nil {
        updateData["phone"] = *updates.Phone
    }
    if updates.IsActive != nil {
        updateData["is_active"] = *updates.IsActive
    }
 
    // Perform update
    if err := tx.Model(&customer).Updates(updateData).Error; err != nil {
        tx.Rollback()
        return nil, fmt.Errorf("failed to update customer: %w", err)
    }
 
    // Commit transaction
    if err := tx.Commit().Error; err != nil {
        return nil, fmt.Errorf("failed to commit update transaction: %w", err)
    }
 
    return &customer, nil
}
 
// Delete soft deletes a customer
func (r *CustomerRepository) Delete(id uint) error {
    result := r.db.Delete(&models.Customer{}, id)
    if result.Error != nil {
        return fmt.Errorf("failed to delete customer: %w", result.Error)
    }
    if result.RowsAffected == 0 {
        return fmt.Errorf("customer with ID %d not found", id)
    }
    return nil
}
 
// HardDelete permanently deletes a customer
func (r *CustomerRepository) HardDelete(id uint) error {
    result := r.db.Unscoped().Delete(&models.Customer{}, id)
    if result.Error != nil {
        return fmt.Errorf("failed to hard delete customer: %w", result.Error)
    }
    if result.RowsAffected == 0 {
        return fmt.Errorf("customer with ID %d not found", id)
    }
    return nil
}
 
// BulkCreate creates multiple customers in a transaction
func (r *CustomerRepository) BulkCreate(customers []models.Customer) error {
    if len(customers) == 0 {
        return nil
    }
 
    return r.db.Transaction(func(tx *gorm.DB) error {
        if err := tx.CreateInBatches(customers, 100).Error; err != nil {
            return fmt.Errorf("failed to bulk create customers: %w", err)
        }
        return nil
    })
}

Complete Application Example

Main Application with Comprehensive CRUD Operations

// cmd/main.go
package main
 
import (
    "fmt"
    "log"
    "os"
 
    "github.com/joho/godotenv"
    "tacnode-gorm-example/internal/config"
    "tacnode-gorm-example/internal/models"
    "tacnode-gorm-example/internal/repository"
)
 
func main() {
    // Load environment variables
    if err := godotenv.Load(); err != nil {
        log.Println("No .env file found")
    }
 
    // Initialize database
    dbConfig := config.LoadDatabaseConfig()
    db, err := config.ConnectDatabase(dbConfig)
    if err != nil {
        log.Fatalf("Failed to connect to database: %v", err)
    }
 
    // Auto-migrate tables
    if err := db.AutoMigrate(&models.Customer{}); err != nil {
        log.Fatalf("Failed to migrate database: %v", err)
    }
 
    // Initialize repository
    customerRepo := repository.NewCustomerRepository(db)
 
    // Run demonstrations
    demonstrateOperations(customerRepo)
}
 
func demonstrateOperations(repo *repository.CustomerRepository) {
    fmt.Println("=== GORM + Tacnode CRUD Demonstration ===\n")
 
    // CREATE operations
    fmt.Println("1. Creating customers...")
    createCustomers(repo)
 
    // READ operations
    fmt.Println("\n2. Reading customers...")
    readCustomers(repo)
 
    // SEARCH operations
    fmt.Println("\n3. Searching customers...")
    searchCustomers(repo)
 
    // UPDATE operations
    fmt.Println("\n4. Updating customer...")
    updateCustomer(repo)
 
    // DELETE operations
    fmt.Println("\n5. Deleting customer...")
    deleteCustomer(repo)
 
    // BULK operations
    fmt.Println("\n6. Bulk operations...")
    bulkOperations(repo)
 
    fmt.Println("\n=== Demonstration completed ===")
}
 
func createCustomers(repo *repository.CustomerRepository) {
    customers := []*models.Customer{
        {
            Name:  "Jacob Emily",
            Email: "jacob.emily@tacnode.io",
            Phone: stringPtr("+1-555-0101"),
        },
        {
            Name:  "Michael Emma",
            Email: "michael.emma@tacnode.io",
            Phone: stringPtr("+1-555-0102"),
        },
    }
 
    for _, customer := range customers {
        if err := repo.Create(customer); err != nil {
            log.Printf("Error creating customer: %v", err)
            continue
        }
        fmt.Printf("Created customer: %s (ID: %d)\n", customer.Name, customer.ID)
    }
}
 
func readCustomers(repo *repository.CustomerRepository) {
    // Get all customers
    customers, total, err := repo.GetAll(10, 0, true)
    if err != nil {
        log.Printf("Error getting customers: %v", err)
        return
    }
 
    fmt.Printf("Found %d active customers (total: %d):\n", len(customers), total)
    for _, customer := range customers {
        phone := "N/A"
        if customer.Phone != nil {
            phone = *customer.Phone
        }
        fmt.Printf("  - %s (%s) | Phone: %s | Active: %t\n",
            customer.Name, customer.Email, phone, customer.IsActive)
    }
 
    // Get by ID
    if len(customers) > 0 {
        customer, err := repo.GetByID(customers[0].ID)
        if err != nil {
            log.Printf("Error getting customer by ID: %v", err)
            return
        }
        fmt.Printf("Retrieved by ID %d: %s\n", customer.ID, customer.Name)
    }
}
 
func searchCustomers(repo *repository.CustomerRepository) {
    searchTerms := []string{"Emily", "tacnode.io", "Michael"}
    
    for _, term := range searchTerms {
        customers, total, err := repo.Search(term, 5, 0)
        if err != nil {
            log.Printf("Error searching for '%s': %v", term, err)
            continue
        }
        
        fmt.Printf("Search '%s': found %d matches (total: %d)\n", term, len(customers), total)
        for _, customer := range customers {
            fmt.Printf("  - %s (%s)\n", customer.Name, customer.Email)
        }
    }
}
 
func updateCustomer(repo *repository.CustomerRepository) {
    // Get a customer to update
    customers, _, err := repo.GetAll(1, 0, true)
    if err != nil || len(customers) == 0 {
        log.Println("No customers found to update")
        return
    }
 
    customer := customers[0]
    originalEmail := customer.Email
    
    updates := &models.CustomerUpdateRequest{
        Email: stringPtr("updated.email@tacnode.io"),
        Phone: stringPtr("+1-555-9999"),
    }
 
    updatedCustomer, err := repo.Update(customer.ID, updates)
    if err != nil {
        log.Printf("Error updating customer: %v", err)
        return
    }
 
    fmt.Printf("Updated customer %d: %s -> %s\n",
        updatedCustomer.ID, originalEmail, updatedCustomer.Email)
}
 
func deleteCustomer(repo *repository.CustomerRepository) {
    // Get a customer to delete
    customers, _, err := repo.GetAll(10, 0, true)
    if err != nil || len(customers) == 0 {
        log.Println("No customers found to delete")
        return
    }
 
    customerToDelete := customers[len(customers)-1]
    
    if err := repo.Delete(customerToDelete.ID); err != nil {
        log.Printf("Error deleting customer: %v", err)
        return
    }
 
    fmt.Printf("Soft deleted customer: %s (ID: %d)\n",
        customerToDelete.Name, customerToDelete.ID)
}
 
func bulkOperations(repo *repository.CustomerRepository) {
    // Bulk create
    bulkCustomers := []models.Customer{
        {Name: "Alice Johnson", Email: "alice.johnson@tacnode.io"},
        {Name: "Bob Wilson", Email: "bob.wilson@tacnode.io"},
        {Name: "Carol Davis", Email: "carol.davis@tacnode.io"},
    }
 
    if err := repo.BulkCreate(bulkCustomers); err != nil {
        log.Printf("Error bulk creating customers: %v", err)
        return
    }
 
    fmt.Printf("Bulk created %d customers\n", len(bulkCustomers))
}
 
// Helper function to create string pointer
func stringPtr(s string) *string {
    return &s
}

Environment Configuration

Create a .env file:

# Database Configuration
DB_HOST=your-cluster.tacnode.io
DB_PORT=5432
DB_USER=your_username
DB_PASSWORD=your_password
DB_NAME=example
DB_SSL_MODE=require

# Connection Pool Settings
DB_MAX_OPEN_CONNS=25
DB_MAX_IDLE_CONNS=5
DB_MAX_IDLE_TIME=15m

# Logging
DB_LOG_LEVEL=info

Sample Output

=== GORM + Tacnode CRUD Demonstration ===

1. Creating customers...
Created customer: Jacob Emily (ID: 1)
Created customer: Michael Emma (ID: 2)

2. Reading customers...
Found 2 active customers (total: 2):
  - Jacob Emily (jacob.emily@tacnode.io) | Phone: +1-555-0101 | Active: true
  - Michael Emma (michael.emma@tacnode.io) | Phone: +1-555-0102 | Active: true
Retrieved by ID 1: Jacob Emily

3. Searching customers...
Search 'Emily': found 1 matches (total: 1)
  - Jacob Emily (jacob.emily@tacnode.io)
Search 'tacnode.io': found 2 matches (total: 2)
  - Jacob Emily (jacob.emily@tacnode.io)
  - Michael Emma (michael.emma@tacnode.io)

4. Updating customer...
Updated customer 1: jacob.emily@tacnode.io -> updated.email@tacnode.io

5. Deleting customer...
Soft deleted customer: Michael Emma (ID: 2)

6. Bulk operations...
Bulk created 3 customers

=== Demonstration completed ===

Best Practices

Performance Optimization

  1. Use Connection Pooling:

    sqlDB.SetMaxOpenConns(25)
    sqlDB.SetMaxIdleConns(5)
    sqlDB.SetConnMaxIdleTime(15 * time.Minute)
  2. Batch Operations:

    db.CreateInBatches(customers, 100)
  3. Prepared Statements:

    gorm.Config{PrepareStmt: true}

Error Handling

Always implement comprehensive error handling:

func (r *CustomerRepository) SafeOperation(id uint) error {
    return r.db.Transaction(func(tx *gorm.DB) error {
        // Your operations here
        return nil
    })
}

Security Considerations

  1. Use Parameterized Queries (GORM handles this automatically)
  2. Validate Input Data:
    type Customer struct {
        Email string `validate:"required,email"`
    }
  3. Implement Proper Access Control
  4. Use Environment Variables for sensitive configuration

This comprehensive guide demonstrates how to build robust, scalable Go applications using GORM with Tacnode. The PostgreSQL compatibility ensures full GORM feature support while providing the benefits of a distributed, cloud-native database architecture.