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
593 lines
13 KiB
Markdown
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
|
|
|