Database & Models
Learn how to define data models and perform database operations using GORM.
Base Modelβ
All models must embed the Base struct:
import "github.com/sven-victor/ez-console/pkg/model"
type Product struct {
model.Base
Name string `gorm:"size:128;not null" json:"name"`
Description string `gorm:"type:text" json:"description"`
Price float64 `gorm:"type:decimal(10,2)" json:"price"`
Stock int `gorm:"default:0" json:"stock"`
}
The Base struct provides:
ID- Internal auto-incrementing primary keyResourceID- Public UUID identifierCreatedAt- Creation timestampUpdatedAt- Last update timestampDeletedAt- Soft delete timestamp
Model Definitionβ
Basic Modelβ
type Category struct {
model.Base
Name string `gorm:"size:64;uniqueIndex" json:"name"`
}
Model with Relationshipsβ
type Product struct {
model.Base
Name string `gorm:"size:128" json:"name"`
Price float64 `gorm:"type:decimal(10,2)" json:"price"`
CategoryID uint `json:"-"`
Category *Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
}
Many-to-Many Relationshipβ
type User struct {
model.Base
Username string `json:"username"`
Roles []Role `gorm:"many2many:user_roles" json:"roles"`
}
type Role struct {
model.Base
Name string `json:"name"`
Users []User `gorm:"many2many:user_roles" json:"-"`
}
GORM Tagsβ
Common tags:
gorm:"size:128"- Column sizegorm:"not null"- NOT NULL constraintgorm:"uniqueIndex"- Unique indexgorm:"default:0"- Default valuegorm:"type:text"- Column typegorm:"foreignKey:CategoryID"- Foreign key
Database Operationsβ
Createβ
product := &Product{
Name: "Laptop",
Price: 999.99,
Stock: 10,
}
db.Create(product)
Readβ
// Find by ResourceID
var product Product
db.Where("resource_id = ?", resourceID).First(&product)
// Find all
var products []Product
db.Find(&products)
// With conditions
db.Where("price > ?", 100).Find(&products)
// With preload
db.Preload("Category").Find(&products)
Updateβ
// Update single field
db.Model(&product).Update("stock", 20)
// Update multiple fields
db.Model(&product).Updates(map[string]interface{}{
"name": "New Name",
"price": 199.99,
})
// Save (updates all fields)
db.Save(&product)
Deleteβ
// Soft delete
db.Delete(&product)
// Permanent delete
db.Unscoped().Delete(&product)
Migrationsβ
Migrations are automatic in EZ-Console:
// Models are automatically migrated on server start
// Add your models in init() function
func init() {
// Register custom models for migration
db.AutoMigrate(&Product{}, &Category{})
}
Queryingβ
Basic Queriesβ
// Find by ID
db.First(&product, 1)
// Find by ResourceID
db.Where("resource_id = ?", resourceID).First(&product)
// Find with condition
db.Where("price > ?", 100).Find(&products)
// Find with multiple conditions
db.Where("price > ? AND stock > ?", 100, 0).Find(&products)
Joinsβ
db.Joins("Category").
Where("categories.name = ?", "Electronics").
Find(&products)
Paginationβ
func (s *ProductService) List(ctx context.Context, page, pageSize int) ([]*Product, int64, error) {
var products []*Product
var total int64
query := s.db.Model(&Product{})
// Count total
query.Count(&total)
// Paginate
offset := (page - 1) * pageSize
err := query.Offset(offset).Limit(pageSize).Find(&products).Error
return products, total, err
}
Transactionsβ
err := db.Transaction(func(tx *gorm.DB) error {
// Create order
if err := tx.Create(&order).Error; err != nil {
return err
}
// Update stock
if err := tx.Model(&Product{}).
Where("id = ?", productID).
Update("stock", gorm.Expr("stock - ?", quantity)).Error; err != nil {
return err
}
return nil
})
Model lifecycle hooks (GORM)β
GORM invokes methods on your model type around create, update, and delete. Use them for defaults, validation that depends on the DB session, cache invalidation, and lightweight invariantsβkeep heavy business rules in services so they stay easy to test.
When you embed model.Base, GORM already maintains ID, ResourceID, timestamps, and soft delete; hooks should not fight that (for example, prefer setting fields the service layer owns rather than duplicating timestamp logic unless you have a specific reason).
BeforeCreateβ
func (p *Product) BeforeCreate(tx *gorm.DB) error {
if p.SKU == "" {
return fmt.Errorf("sku is required")
}
return nil
}
AfterCreate / AfterUpdate / AfterDeleteβ
Typical uses: notify downstream systems, refresh caches, enqueue work.
func (p *Product) AfterUpdate(tx *gorm.DB) error {
cache.Delete("product:" + p.ResourceID)
return nil
}
BeforeUpdate / BeforeDeleteβ
Use for guarding deletes or logging field-level changes (tx.Statement.Changed("Price")). Return an error to abort the operation.
More background: Hooks & Events (service-level hooks) and the GORM docs for the full callback list.
Database Configurationβ
SQLite (Default)β
database:
driver: sqlite
path: ./app.db
MySQLβ
database:
driver: mysql
host: localhost
port: 3306
username: root
password: password
dbname: myapp
PostgreSQLβ
database:
driver: postgres
host: localhost
port: 5432
username: postgres
password: password
dbname: myapp
sslmode: disable
Best Practicesβ
DO β β
- Always embed
model.Base - Use proper GORM tags
- Define indexes for frequently queried fields
- Use transactions for multi-step operations
- Use ResourceID for external APIs
- Preload relationships to avoid N+1 queries
DON'T ββ
- Don't expose internal ID in APIs
- Don't forget to handle errors
- Don't use
SELECT *unnecessarily - Don't perform operations in loops (use batch operations)
- Don't ignore database constraints
Next Stepsβ
- Service-level extension points: Hooks & Events
- Learn about request validation
- Implement error handling
- Add audit logging
- Explore API best practices