Contrib Packages
Stable v1.4.8
Database
Learn about Database in EchoNext.
Database
EchoNext provides optional database utilities through the pkg/contrib/database package, including GORM helpers, repository pattern, and Atlas integration for declarative schema migrations.
Overview
The database package provides:
- Connection Management - Configure and connect to databases with retry logic
- Repository Pattern - Generic CRUD operations with
Repository[T] - Transactions - Utilities for managing database transactions
- Migrations - Traditional migration helpers
- Atlas Integration - Declarative schema management with Atlas
Installation
go get github.com/abdussamadbello/echonext/pkg/contrib/database@v1.4.8Connection Management
Basic Connection
import "github.com/abdussamadbello/echonext/pkg/contrib/database"
config := &database.Config{
Driver: "postgres",
Host: "localhost",
Port: 5432,
User: "postgres",
Password: "password",
Database: "myapp",
SSLMode: "disable",
}
db, err := database.Connect(config)
if err != nil {
log.Fatal(err)
}Connection with Pool Configuration
config := &database.Config{
Driver: "postgres",
Host: "localhost",
Port: 5432,
User: "postgres",
Password: "password",
Database: "myapp",
// Connection pool settings
MaxOpenConns: 25,
MaxIdleConns: 5,
ConnMaxLifetime: time.Hour,
ConnMaxIdleTime: 30 * time.Minute,
}
db, err := database.Connect(config)Repository Pattern
The generic Repository[T] provides type-safe CRUD operations:
import "github.com/abdussamadbello/echonext/pkg/contrib/database"
// Define your model
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
Email string `gorm:"unique;not null"`
CreatedAt time.Time
UpdatedAt time.Time
}
// Create repository
userRepo := database.NewRepository[User](db)
// Create
user := &User{Name: "John", Email: "john@example.com"}
err := userRepo.Create(user)
// Find by ID
user, err := userRepo.FindByID(1)
// Find all
users, err := userRepo.FindAll()
// Find with conditions
users, err := userRepo.FindWhere("email = ?", "john@example.com")
// Update
user.Name = "John Doe"
err := userRepo.Update(user)
// Delete
err := userRepo.Delete(user)Custom Queries
// First matching record
user, err := userRepo.First("email = ?", "john@example.com")
// Count records
count, err := userRepo.Count("active = ?", true)
// Exists check
exists, err := userRepo.Exists("email = ?", "john@example.com")Transactions
Using WithTx
import "github.com/abdussamadbello/echonext/pkg/contrib/database"
err := database.WithTx(db, func(tx *gorm.DB) error {
// All operations use the same transaction
userRepo := database.NewRepository[User](tx)
orderRepo := database.NewRepository[Order](tx)
user := &User{Name: "Jane"}
if err := userRepo.Create(user); err != nil {
return err // Transaction rolls back
}
order := &Order{UserID: user.ID, Total: 100}
if err := orderRepo.Create(order); err != nil {
return err // Transaction rolls back
}
return nil // Transaction commits
})Using WithTxResult
result, err := database.WithTxResult(db, func(tx *gorm.DB) (*Order, error) {
userRepo := database.NewRepository[User](tx)
orderRepo := database.NewRepository[Order](tx)
user := &User{Name: "Jane"}
if err := userRepo.Create(user); err != nil {
return nil, err
}
order := &Order{UserID: user.ID, Total: 100}
if err := orderRepo.Create(order); err != nil {
return nil, err
}
return order, nil
})Atlas Integration
EchoNext integrates with Atlas for declarative, version-controlled database schema management.
Why Atlas?
- Declarative Schema - Define desired state in HCL, generate migrations automatically
- Versioned Migrations - SQL files with checksum verification
- Migration Linting - Detect destructive changes before they happen
- Multi-Environment - Separate configs for local, staging, production
Prerequisites
Install Atlas CLI:
# macOS
brew install ariga/tap/atlas
# Linux
curl -sSf https://atlasgo.sh | sh
# Docker
docker pull arigaio/atlasBasic Setup
import "github.com/abdussamadbello/echonext/pkg/contrib/database"
// Check if Atlas is installed
if !database.IsAtlasInstalled() {
fmt.Println(database.InstallAtlas())
return
}
// Create Atlas instance
atlas := database.NewAtlas(&database.AtlasConfig{
Dir: "migrations",
ConfigFile: "atlas.hcl",
Env: "local",
})Configuration
AtlasConfig Options:
type AtlasConfig struct {
Dir string // Path to migrations directory (default: "migrations")
URL string // Database connection URL
DevURL string // Dev database URL for schema calculations
Env string // Atlas environment (local, staging, production)
ConfigFile string // Path to atlas.hcl (default: "atlas.hcl")
DryRun bool // Enable dry-run mode
Verbose bool // Enable verbose output
}Using Defaults:
atlas := database.NewAtlas(database.DefaultAtlasConfig())Migration Operations
Check Status
ctx := context.Background()
status, err := atlas.Status(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Current: %s\n", status.Current)
fmt.Printf("Pending: %d migrations\n", len(status.Pending))Apply Migrations
// Apply all pending migrations
err := atlas.Apply(ctx)
// Apply N migrations
err := atlas.ApplyN(ctx, 2)Rollback Migrations
// Rollback last migration
err := atlas.Down(ctx)
// Rollback N migrations
err := atlas.DownN(ctx, 3)Generate New Migration
// Generate migration from schema changes
output, err := atlas.Diff(ctx, "add_users_table")
if err != nil {
log.Fatal(err)
}
fmt.Println(output)Create Empty Migration
// Create empty migration file
path, err := atlas.New(ctx, "add_index")
fmt.Printf("Created: %s\n", path)Lint Migrations
// Check for issues
err := atlas.Lint(ctx)
if err != nil {
fmt.Printf("Lint issues: %v\n", err)
}Validate Migrations
// Validate migration directory
err := atlas.Validate(ctx)Inspect Schema
// Get current database schema as SQL
schema, err := atlas.SchemaInspect(ctx)
fmt.Println(schema)Schema Definition (schema.hcl)
Define your database schema declaratively:
// schema.hcl
table "users" {
schema = schema.public
column "id" {
null = false
type = bigserial
}
column "email" {
null = false
type = varchar(255)
}
column "name" {
null = false
type = varchar(100)
}
column "created_at" {
null = false
type = timestamptz
default = sql("now()")
}
primary_key {
columns = [column.id]
}
index "idx_users_email" {
unique = true
columns = [column.email]
}
}
table "posts" {
schema = schema.public
column "id" {
null = false
type = bigserial
}
column "user_id" {
null = false
type = bigint
}
column "title" {
null = false
type = varchar(255)
}
column "content" {
null = false
type = text
}
primary_key {
columns = [column.id]
}
foreign_key "fk_posts_user" {
columns = [column.user_id]
ref_columns = [table.users.column.id]
on_delete = CASCADE
}
}
schema "public" {}Atlas Configuration (atlas.hcl)
Configure environments and settings:
// atlas.hcl
// Local development
env "local" {
src = "file://schema.hcl"
url = "postgres://postgres:password@localhost:5432/myapp?sslmode=disable"
dev = "docker://postgres/16/dev?search_path=public"
migration {
dir = "file://migrations"
}
}
// Staging environment
env "staging" {
src = "file://schema.hcl"
url = getenv("STAGING_DATABASE_URL")
migration {
dir = "file://migrations"
}
}
// Production with safety checks
env "production" {
src = "file://schema.hcl"
url = getenv("PRODUCTION_DATABASE_URL")
migration {
dir = "file://migrations"
}
// Prevent destructive changes
diff {
skip {
drop_column = true
drop_table = true
}
}
}
// Lint configuration
lint {
destructive {
error = true
}
data_depend {
error = true
}
}CLI Commands
EchoNext CLI provides Atlas integration commands:
# Initialize Atlas setup
echonext db init
# Apply migrations
echonext db migrate
echonext db migrate --dry-run
echonext db migrate --env=production
# Check status
echonext db migrate:status
# Create new migration
echonext db migrate:new add_posts_table
# Generate from schema changes
echonext db migrate:diff add_email_index
# Rollback
echonext db migrate:down --count=1
# Lint for issues
echonext db migrate:lint
# Inspect current schema
echonext db schema:inspectMigration Workflow
Recommended workflow:
- Modify schema.hcl with your desired changes
- Generate migration:
echonext db migrate:diff describe_change - Review the generated SQL in
migrations/ - Apply:
echonext db migrate
Example:
# Add a new column to users table in schema.hcl
# ...edit schema.hcl...
# Generate migration
echonext db migrate:diff add_phone_column
# Review generated migration
cat migrations/XXXXXX_add_phone_column.sql
# Apply
echonext db migrateEnvironment Variables
Configure Atlas via environment variables:
export DATABASE_URL="postgres://user:pass@localhost:5432/myapp"
export STAGING_DATABASE_URL="postgres://user:pass@staging:5432/myapp"
export PRODUCTION_DATABASE_URL="postgres://user:pass@prod:5432/myapp"Initialization Helper
Initialize a new migration directory:
err := database.InitMigrationDir("migrations")
if err != nil {
log.Fatal(err)
}Error Handling
// Check Atlas installation before running commands
if err := database.EnsureAtlasInstalled(); err != nil {
log.Fatal(err)
}
// Handle migration errors
if err := atlas.Apply(ctx); err != nil {
// Check if it's a connection error, migration conflict, etc.
log.Printf("Migration failed: %v", err)
}Dry Run Mode
Test migrations without applying:
atlas := database.NewAtlas(&database.AtlasConfig{
Dir: "migrations",
Env: "local",
DryRun: true,
Verbose: true,
})
// Shows what would be applied without making changes
err := atlas.Apply(ctx)Best Practices
Database Migrations
- Always use version control for migrations
- Never modify existing migrations - create new ones
- Test migrations locally before applying to staging/production
- Use dry-run mode to preview changes
- Enable linting to catch destructive operations
Repository Pattern
- One repository per model for clear separation
- Use transactions for multi-table operations
- Handle errors appropriately - check for
gorm.ErrRecordNotFound - Use proper indexing in your models
Connections
- Configure pool sizes based on workload
- Set connection timeouts to prevent hanging
- Use connection pooling in production
- Close connections gracefully on shutdown