mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
- Initialize git repository - Add comprehensive .gitignore for Go projects - Install golangci-lint v2.6.0 (latest v2) globally - Configure .golangci.yml with appropriate linters and formatters - Fix all formatting issues (gofmt) - Fix all errcheck issues (unchecked errors) - Adjust complexity threshold for validation functions - All checks passing: build, test, vet, lint
13 KiB
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
- 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
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
- Use Select - only select fields you need
- Use Preload - for eager loading relationships
- Batch operations - use
CreateInBatchesfor bulk inserts - Indexes - create indexes on frequently queried fields
- Connection pooling - configure
SetMaxOpenConns,SetMaxIdleConns - Avoid N+1 queries - use
Preloadinstead 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
- 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
- Use transactions for multi-table operations
- Validate before save using hooks or middleware
- Use indexes on foreign keys and frequently queried fields
- Preload relationships to avoid N+1 queries
- Use Select to limit retrieved fields
- Context support - always use
WithContext(ctx)for cancellation - Connection pooling - configure appropriately for your workload
- Logging - enable SQL logging in development, disable in production