turash/docs/dev_guides/03_gorm.md
Damir Mukimov 000eab4740
Major repository reorganization and missing backend endpoints implementation
Repository Structure:
- Move files from cluttered root directory into organized structure
- Create archive/ for archived data and scraper results
- Create bugulma/ for the complete application (frontend + backend)
- Create data/ for sample datasets and reference materials
- Create docs/ for comprehensive documentation structure
- Create scripts/ for utility scripts and API tools

Backend Implementation:
- Implement 3 missing backend endpoints identified in gap analysis:
  * GET /api/v1/organizations/{id}/matching/direct - Direct symbiosis matches
  * GET /api/v1/users/me/organizations - User organizations
  * POST /api/v1/proposals/{id}/status - Update proposal status
- Add complete proposal domain model, repository, and service layers
- Create database migration for proposals table
- Fix CLI server command registration issue

API Documentation:
- Add comprehensive proposals.md API documentation
- Update README.md with Users and Proposals API sections
- Document all request/response formats, error codes, and business rules

Code Quality:
- Follow existing Go backend architecture patterns
- Add proper error handling and validation
- Match frontend expected response schemas
- Maintain clean separation of concerns (handler -> service -> repository)
2025-11-25 06:01:16 +01:00

13 KiB

GORM Development Guide

Library: gorm.io/gorm
Used In: MVP - PostgreSQL ORM (optional, for simpler CRUD)
Purpose: Object-Relational Mapping for PostgreSQL database operations


Where It's Used

  • Alternative to pgx for simpler CRUD operations
  • Site geospatial data (PostGIS sync from Neo4j)
  • User management
  • Simple relational queries
  • Can use alongside pgx for raw queries

Official Documentation


Installation

go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres

Key Concepts

1. Database Connection

import (
    "gorm.io/gorm"
    "gorm.io/driver/postgres"
)

func NewDB(dsn string) (*gorm.DB, error) {
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Info),
    })
    if err != nil {
        return nil, err
    }
    
    // Get underlying sql.DB for connection pool settings
    sqlDB, err := db.DB()
    if err != nil {
        return nil, err
    }
    
    // Set connection pool settings
    sqlDB.SetMaxIdleConns(10)
    sqlDB.SetMaxOpenConns(100)
    sqlDB.SetConnMaxLifetime(time.Hour)
    
    return db, nil
}

2. Model Definition

// Basic model
type Business struct {
    ID        uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
    Name      string    `gorm:"not null"`
    Email     string    `gorm:"uniqueIndex;not null"`
    Phone     string
    CreatedAt time.Time
    UpdatedAt time.Time
}

// With relationships
type Site struct {
    ID        uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
    BusinessID uuid.UUID `gorm:"type:uuid;not null"`
    Address   string    `gorm:"not null"`
    Latitude  float64
    Longitude float64
    
    Business  Business `gorm:"foreignKey:BusinessID"`
    CreatedAt time.Time
    UpdatedAt time.Time
}

// Table name customization
func (Business) TableName() string {
    return "businesses"
}

3. Auto Migration

// Migrate all models
err := db.AutoMigrate(&Business{}, &Site{}, &ResourceFlow{})
if err != nil {
    log.Fatal(err)
}

// With custom migration options
err := db.AutoMigrate(&Business{})

4. CRUD Operations

// Create
business := Business{
    Name:  "Factory A",
    Email: "contact@factorya.com",
}

result := db.Create(&business)
if result.Error != nil {
    return result.Error
}
// business.ID is automatically filled

// Create with specific fields
db.Select("Name", "Email").Create(&business)

// Create multiple
businesses := []Business{
    {Name: "Factory A", Email: "a@example.com"},
    {Name: "Factory B", Email: "b@example.com"},
}
db.Create(&businesses)

// Batch insert
db.CreateInBatches(&businesses, 100)

5. Read Operations

// Find by ID
var business Business
result := db.First(&business, id)
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
    // Not found
}
// or
db.First(&business, "id = ?", id)

// Find with conditions
var businesses []Business
db.Where("name = ?", "Factory A").Find(&businesses)

// Multiple conditions
db.Where("name = ? AND email = ?", "Factory A", "a@example.com").Find(&businesses)

// Or conditions
db.Where("name = ? OR email = ?", "Factory A", "b@example.com").Find(&businesses)

// Select specific fields
db.Select("id", "name").Find(&businesses)

// Limit and offset
db.Limit(10).Offset(20).Find(&businesses)

// Order by
db.Order("created_at DESC").Find(&businesses)

// Count
var count int64
db.Model(&Business{}).Where("name LIKE ?", "%Factory%").Count(&count)

6. Update Operations

// Update single record
db.Model(&business).Update("name", "New Name")

// Update multiple fields
db.Model(&business).Updates(Business{
    Name:  "New Name",
    Email: "new@example.com",
})

// Update only non-zero fields
db.Model(&business).Updates(map[string]interface{}{
    "name": "New Name",
})

// Update all matching records
db.Model(&Business{}).Where("name = ?", "Old Name").Update("name", "New Name")

// Save (updates all fields)
db.Save(&business)

7. Delete Operations

// Soft delete (if DeletedAt field exists)
db.Delete(&business)

// Hard delete
db.Unscoped().Delete(&business)

// Delete with conditions
db.Where("name = ?", "Factory A").Delete(&Business{})

// Delete all
db.Where("1 = 1").Delete(&Business{})

8. Relationships

// Has Many relationship
type Business struct {
    ID    uuid.UUID
    Name  string
    Sites []Site `gorm:"foreignKey:BusinessID"`
}

// Belongs To relationship
type Site struct {
    ID        uuid.UUID
    BusinessID uuid.UUID
    Business  Business `gorm:"foreignKey:BusinessID"`
}

// Preload relationships
var business Business
db.Preload("Sites").First(&business, id)

// Eager loading
db.Preload("Sites").Preload("Sites.ResourceFlows").Find(&businesses)

// Joins
var sites []Site
db.Joins("Business").Find(&sites)

9. Transactions

// Transaction
err := db.Transaction(func(tx *gorm.DB) error {
    // Create business
    if err := tx.Create(&business).Error; err != nil {
        return err // Rollback automatically
    }
    
    // Create site
    site := Site{BusinessID: business.ID}
    if err := tx.Create(&site).Error; err != nil {
        return err // Rollback automatically
    }
    
    return nil // Commit
})

// Manual transaction
tx := db.Begin()
if err := tx.Create(&business).Error; err != nil {
    tx.Rollback()
    return err
}
if err := tx.Create(&site).Error; err != nil {
    tx.Rollback()
    return err
}
tx.Commit()

10. Raw SQL

// Raw query
var businesses []Business
db.Raw("SELECT * FROM businesses WHERE name = ?", "Factory A").Scan(&businesses)

// Raw with SQL
db.Exec("UPDATE businesses SET name = ? WHERE id = ?", "New Name", id)

// Row scan
type Result struct {
    Name  string
    Count int
}
var result Result
db.Raw("SELECT name, COUNT(*) as count FROM businesses GROUP BY name").Scan(&result)

MVP-Specific Patterns

Site Geospatial Service

type SiteGeoService struct {
    db *gorm.DB
}

type SiteGeo struct {
    SiteID     uuid.UUID `gorm:"type:uuid;primary_key"`
    BusinessID uuid.UUID `gorm:"type:uuid;not null;index"`
    Latitude   float64   `gorm:"not null"`
    Longitude  float64   `gorm:"not null"`
    Location   postgis.Geometry `gorm:"type:geometry(Point,4326);not null;index:idx_location"`
    UpdatedAt  time.Time
}

// Create site with PostGIS
func (s *SiteGeoService) Create(ctx context.Context, site SiteGeo) error {
    // Set location from lat/lon
    site.Location = postgis.NewPoint(site.Longitude, site.Latitude)
    
    return s.db.WithContext(ctx).Create(&site).Error
}

// Find sites within radius (PostGIS)
func (s *SiteGeoService) FindWithinRadius(ctx context.Context, lat, lon float64, radiusMeters float64) ([]SiteGeo, error) {
    var sites []SiteGeo
    
    query := `
        SELECT * FROM site_geos
        WHERE ST_DWithin(
            location,
            ST_MakePoint(?, ?)::geometry,
            ?
        )
        ORDER BY ST_Distance(location, ST_MakePoint(?, ?)::geometry)
    `
    
    err := s.db.WithContext(ctx).
        Raw(query, lon, lat, radiusMeters, lon, lat).
        Scan(&sites).
        Error
    
    return sites, err
}

Repository Pattern

type BusinessRepository struct {
    db *gorm.DB
}

func (r *BusinessRepository) Create(ctx context.Context, business *Business) error {
    return r.db.WithContext(ctx).Create(business).Error
}

func (r *BusinessRepository) FindByID(ctx context.Context, id uuid.UUID) (*Business, error) {
    var business Business
    err := r.db.WithContext(ctx).First(&business, id).Error
    if errors.Is(err, gorm.ErrRecordNotFound) {
        return nil, ErrNotFound
    }
    return &business, err
}

func (r *BusinessRepository) FindAll(ctx context.Context, limit, offset int) ([]Business, error) {
    var businesses []Business
    err := r.db.WithContext(ctx).
        Limit(limit).
        Offset(offset).
        Find(&businesses).
        Error
    return businesses, err
}

func (r *BusinessRepository) Update(ctx context.Context, business *Business) error {
    return r.db.WithContext(ctx).Save(business).Error
}

func (r *BusinessRepository) Delete(ctx context.Context, id uuid.UUID) error {
    return r.db.WithContext(ctx).Delete(&Business{}, id).Error
}

Advanced Features

1. Hooks (Lifecycle Callbacks)

// Before Create
func (b *Business) BeforeCreate(tx *gorm.DB) error {
    if b.ID == uuid.Nil {
        b.ID = uuid.New()
    }
    return nil
}

// After Create
func (b *Business) AfterCreate(tx *gorm.DB) error {
    // Send notification, etc.
    return nil
}

// Before Update
func (b *Business) BeforeUpdate(tx *gorm.DB) error {
    b.UpdatedAt = time.Now()
    return nil
}

2. Validation

import "github.com/go-playground/validator/v10"

// Custom validator
type Business struct {
    Email string `gorm:"uniqueIndex" validate:"required,email"`
    Name  string `validate:"required,min=3,max=100"`
}

// Validate before save
func (b *Business) BeforeCreate(tx *gorm.DB) error {
    validate := validator.New()
    return validate.Struct(b)
}

3. Scopes

// Reusable query scope
func ActiveBusinesses(db *gorm.DB) *gorm.DB {
    return db.Where("status = ?", "active")
}

// Usage
db.Scopes(ActiveBusinesses).Find(&businesses)

// Multiple scopes
func WithSites(db *gorm.DB) *gorm.DB {
    return db.Preload("Sites")
}

db.Scopes(ActiveBusinesses, WithSites).Find(&businesses)

4. Preloading with Conditions

// Preload with conditions
db.Preload("Sites", "status = ?", "active").Find(&businesses)

// Preload with custom query
db.Preload("Sites", func(db *gorm.DB) *gorm.DB {
    return db.Where("latitude > ?", 0).Order("created_at DESC")
}).Find(&businesses)

5. Association Operations

// Append association
business := Business{ID: businessID}
db.First(&business)
site := Site{Address: "New Address"}
db.Model(&business).Association("Sites").Append(&site)

// Replace association
db.Model(&business).Association("Sites").Replace(&site1, &site2)

// Delete association
db.Model(&business).Association("Sites").Delete(&site)

// Clear all associations
db.Model(&business).Association("Sites").Clear()

// Count associations
count := db.Model(&business).Association("Sites").Count()

Performance Tips

  1. Use Select - only select fields you need
  2. Use Preload - for eager loading relationships
  3. Batch operations - use CreateInBatches for bulk inserts
  4. Indexes - create indexes on frequently queried fields
  5. Connection pooling - configure SetMaxOpenConns, SetMaxIdleConns
  6. Avoid N+1 queries - use Preload instead of lazy loading

Soft Deletes

type Business struct {
    ID        uuid.UUID
    DeletedAt gorm.DeletedAt `gorm:"index"`
    // ... other fields
}

// Soft delete
db.Delete(&business)

// Find with soft deleted
db.Unscoped().Find(&businesses)

// Permanently delete
db.Unscoped().Delete(&business)

Migrations

// Auto migrate (for development)
db.AutoMigrate(&Business{}, &Site{})

// Manual migration (for production)
migrator := db.Migrator()

// Create table
migrator.CreateTable(&Business{})

// Check if table exists
if !migrator.HasTable(&Business{}) {
    migrator.CreateTable(&Business{})
}

// Add column
if !migrator.HasColumn(&Business{}, "phone") {
    migrator.AddColumn(&Business{}, "phone")
}

// Create index
migrator.CreateIndex(&Business{}, "idx_email")

Error Handling

result := db.Create(&business)
if result.Error != nil {
    if errors.Is(result.Error, gorm.ErrDuplicatedKey) {
        // Handle duplicate key
    } else if errors.Is(result.Error, gorm.ErrRecordNotFound) {
        // Handle not found
    }
    return result.Error
}

// Check if record exists
if errors.Is(db.First(&business, id).Error, gorm.ErrRecordNotFound) {
    // Not found
}

Tutorials & Resources


Best Practices

  1. Use transactions for multi-table operations
  2. Validate before save using hooks or middleware
  3. Use indexes on foreign keys and frequently queried fields
  4. Preload relationships to avoid N+1 queries
  5. Use Select to limit retrieved fields
  6. Context support - always use WithContext(ctx) for cancellation
  7. Connection pooling - configure appropriately for your workload
  8. Logging - enable SQL logging in development, disable in production