Go dan GORM: Kombinasi yang Produktif
GORM adalah ORM paling populer di ekosistem Go. Dengan fitur yang lengkap auto migration, associations, hooks, dan caching GORM mempercepat development tanpa mengorbankan fleksibilitas. Saya sudah pakai GORM di lebih dari 10 proyek production.
Setup Database Connection
package database
import (
"fmt"
"os"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var DB *gorm.DB
func Connect() error {
dsn := fmt.Sprintf(
"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable",
os.Getenv("DB_HOST"), os.Getenv("DB_USER"),
os.Getenv("DB_PASSWORD"), os.Getenv("DB_NAME"),
os.Getenv("DB_PORT"),
)
var err error
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
return fmt.Errorf("failed to connect database: %w", err)
}
return nil
}
Define Models
package models
import "gorm.io/gorm"
type User struct {
gorm.Model
Name string `json:"name" gorm:"size:100;not null"`
Email string `json:"email" gorm:"uniqueIndex;size:255;not null"`
Password string `json:"-" gorm:"not null"`
Projects []Project `json:"projects" gorm:"foreignKey:UserID"`
}
type Project struct {
gorm.Model
Name string `json:"name" gorm:"size:200;not null"`
Description string `json:"description" gorm:"type:text"`
UserID uint `json:"user_id" gorm:"index;not null"`
Tasks []Task `json:"tasks" gorm:"foreignKey:ProjectID"`
}
type Task struct {
gorm.Model
Title string `json:"title" gorm:"size:200;not null"`
Done bool `json:"done" gorm:"default:false"`
ProjectID uint `json:"project_id" gorm:"index;not null"`
}
CRUD Handlers
func ListProjects(c *fiber.Ctx) error {
var projects []models.Project
page, _ := strconv.Atoi(c.Query("page", "1"))
limit, _ := strconv.Atoi(c.Query("limit", "10"))
offset := (page - 1) * limit
database.DB.Preload("Tasks").Offset(offset).Limit(limit).Find(&projects)
return c.JSON(projects)
}
func CreateProject(c *fiber.Ctx) error {
project := new(models.Project)
if err := c.BodyParser(project); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"})
}
database.DB.Create(&project)
return c.Status(201).JSON(project)
}
func GetProject(c *fiber.Ctx) error {
id := c.Params("id")
var project models.Project
if err := database.DB.Preload("Tasks").First(&project, id).Error; err != nil {
return c.Status(404).JSON(fiber.Map{"error": "Project not found"})
}
return c.JSON(project)
}
Best Practices yang Wajib Diterapkan
- Selalu pakai Preload() untuk eager loading relations jangan biarkan N+1 query
- Gunakan transactions untuk operasi yang butuh atomicity
- Validate input sebelum ke database gunakan Go validator tags
- Return HTTP status codes yang benar 201 Created untuk POST, 404 untuk not found
- Pakai context untuk cancel queries yang lama
GORM memang powerful, tapi ingat: ORM bukan silver bullet. Untuk query yang kompleks, jangan takut pakai raw SQL.
GORM Hooks untuk Business Logic
GORM hooks memungkinkan Anda menjalankan kode sebelum atau sesudah operasi database. Ini powerful untuk implementasi business logic yang terkait dengan data lifecycle:
// Hook: before create user, hash password
func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.Password != "" {
hashed, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = string(hashed)
}
return nil
}
// Hook: after create project, send welcome notification
func (p *Project) AfterCreate(tx *gorm.DB) error {
return notifyUser(p.UserID, "Project "+p.Name+" created!")
}
Hooks membuat code lebih clean karena business logic terkait data lifecycle terpusat di model, bukan tersebar di berbagai handler. Tapi hati-hati: jangan terlalu banyak pakai hooks karena bisa membuat flow tidak predictable.
Performance Optimization untuk Query Kompleks
Untuk query yang sangat kompleks dan butuh performa optimal, jangan ragu pakai raw SQL. GORM support hybrid approach:
func GetRevenueReport(db *gorm.DB, startDate, endDate time.Time) ([]RevenueRow, error) {
var results []RevenueRow
err := db.Raw(`
SELECT DATE_TRUNC('month', created_at) as month,
COUNT(*) as total_subscriptions,
SUM(amount) as total_revenue
FROM subscriptions
WHERE created_at BETWEEN ? AND ? AND status = ?
GROUP BY DATE_TRUNC('month', created_at)
ORDER BY month
`, startDate, endDate, "active").Scan(&results).Error
return results, err
}