turash/dev_guides/03_gorm.md
Damir Mukimov 4a2fda96cd
Initial commit: Repository setup with .gitignore, golangci-lint v2.6.0, and code quality checks
- 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
2025-11-01 07:36:22 +01:00

593 lines
13 KiB
Markdown

# 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