# 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 - **GitHub**: https://github.com/go-gorm/gorm - **Official Docs**: https://gorm.io/docs/ - **GoDoc**: https://pkg.go.dev/gorm.io/gorm - **Getting Started**: https://gorm.io/docs/connecting_to_the_database.html --- ## Installation ```bash go get -u gorm.io/gorm go get -u gorm.io/driver/postgres ``` --- ## Key Concepts ### 1. Database Connection ```go 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go 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 ```go 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) ```go // 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 ```go 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 ```go // 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 ```go // 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 ```go // 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 ```go 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 ```go // 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 ```go 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 - **Official Examples**: https://github.com/go-gorm/gorm/tree/master/examples - **Getting Started**: https://gorm.io/docs/index.html - **Associations**: https://gorm.io/docs/belongs_to.html - **Query**: https://gorm.io/docs/query.html - **Advanced Topics**: https://gorm.io/docs/index.html#Advanced-Topics --- ## 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