How to Handle Database Migrations with domainx in Gorig: A Complete Guide
Use domainx.AutoMigrate in your model package's init() function to register schema changes, then call domainx.Start() in your main function to automatically execute migrations against your configured database service.
Handling database migrations with domainx in Gorig provides a declarative, driver-agnostic approach to schema management. The jom-io/gorig framework abstracts migration complexity through its domainx package, allowing you to define model changes alongside your domain logic while the framework handles execution timing and database-specific implementation details.
Understanding the domainx Migration Architecture
The migration system centers on a lightweight registry pattern that separates migration definitions from execution logic, defined primarily in domainx/migration.go and orchestrated through domainx/service.go.
Core Components
The Migration struct (lines 19-22 in domainx/migration.go) encapsulates a DBFunc returning a ConTable and an optional index list. These instances are collected in the global MigrationList slice (line 17), which acts as the central registry for all pending migrations.
The DBService interface (lines 76-80 in domainx/service.go) defines the contract for concrete database drivers:
type DBService interface {
Name() string
Migrate(con *Con, tableName string, value ConTable, indexList []Index) error
// ... additional methods
}
Migration Execution Flow
When your application starts, the following sequence occurs:
- Registration: During package initialization,
domainx.AutoMigrate(lines 61-67 indomainx/api.go) appends migration entries toMigrationList. - Startup:
domainx.Startinvokesservice.Start, which initializes all database connections. - Execution: After a 1-second grace period (lines 29-34 in
domainx/service.go), the system iterates throughMigrationListand callsservice.Migratefor each entry. - Resolution:
service.Migrate(lines 47-73) resolves the appropriateDBServiceimplementation and delegates schema creation.
Registering Migrations with AutoMigrate
To handle database migrations with domainx in Gorig, you define models that embed domainx.Con and register them using the AutoMigrate helper.
Defining a Model
Create a struct that embeds domainx.Con and implements the ConTable interface:
package model
import "github.com/jom-io/gorig/domainx"
type User struct {
domainx.Con // Embeds base Con with connection metadata
Name string `gorm:"size:64"`
Email string `gorm:"size:128;uniqueIndex"`
}
// TableName satisfies the ConTable interface
func (User) TableName() string {
return "users"
}
Registering the Migration
In the same package, use an init function to register the migration:
func init() {
domainx.AutoMigrate(
func() domainx.ConTable { return &User{} },
// Define a unique index on the Email column using CtIdx
domainx.CtIdx(domainx.Unique, "email"),
)
}
The CtIdx function (lines 36-58 in domainx/api.go) constructs an Index value specifying the index type (Unique or standard), field names, and optional custom name. When AutoMigrate executes, it creates a Migration instance containing your function and index definitions, appending it to the global MigrationList.
Implementing Database-Specific Migration Logic
While domainx handles orchestration, concrete schema operations are implemented by services satisfying the DBService interface.
The DBService Interface Contract
As defined in domainx/service.go (lines 76-80), your database driver must implement:
type DBService interface {
Name() string
Migrate(con *Con, tableName string, value ConTable, indexList []Index) error
// Additional methods for queries, transactions, etc.
}
The Migrate method receives the connection configuration, table name, model instance, and any indexes defined via CtIdx.
Example MySQL Implementation
Below is a simplified implementation showing how to handle the Migrate call for MySQL using GORM:
type MySQLService struct {
db *gorm.DB
}
func (s *MySQLService) Migrate(
con *domainx.Con,
tableName string,
value domainx.ConTable,
indexList []domainx.Index,
) error {
// Auto-create or update table schema
if err := s.db.AutoMigrate(value); err != nil {
return fmt.Errorf("auto migrate failed: %w", err)
}
// Apply custom indexes defined via CtIdx
for _, idx := range indexList {
if idx.IdxType == domainx.Unique {
// Create unique index on specified fields
s.db.Model(value).Clauses(
clause.Unique{Column: clause.Column{Name: idx.Fields[0]}},
).Create(value)
}
}
return nil
}
Register this service during bootstrap:
func init() {
domainx.RegisterDBService(domainx.Mysql, &MySQLService{db: gormDB})
}
Starting the Application and Executing Migrations
Once models and services are registered, starting the application triggers the migration process automatically.
In your main.go:
func main() {
// domainx.Start initializes connections and runs pending migrations
if err := domainx.Start("", "8080"); err != nil {
log.Fatalf("failed to start domainx: %v", err)
}
}
According to the source in domainx/service.go (lines 29-34), service.Start waits approximately 1 second after initializing database connections before iterating over MigrationList. This grace period ensures all DBService implementations are fully registered and connected before schema modifications begin.
Each registered migration is then executed via service.Migrate (lines 47-73), which:
- Invokes the migration's
DBFuncto obtain theConTableinstance - Extracts the connection via
GetCon() - Dispatches to the appropriate
DBService.Migrateimplementation based oncon.GetConType()
Summary
Handling database migrations with domainx in Gorig follows a declarative, service-oriented pattern:
- Define models by embedding
domainx.Conand implementing theConTableinterface with aTableName()method - Register migrations using
domainx.AutoMigratein packageinit()functions, optionally specifying indexes viaCtIdx - Implement drivers by satisfying the
DBServiceinterface, specifically implementing theMigratemethod to handle schema creation - Execute automatically by calling
domainx.Start, which applies all pending migrations fromMigrationListafter a brief initialization delay
This architecture keeps migration logic co-located with domain models while remaining agnostic to the underlying database engine.
Frequently Asked Questions
How does domainx track which migrations have already been applied?
The domainx package does not maintain a separate schema migrations table like traditional tools such as Flyway or Liquibase. Instead, it relies on the underlying database driver's Migrate implementation—such as GORM's AutoMigrate—to perform idempotent schema changes. Each time domainx.Start runs, it re-applies all registered migrations from MigrationList, and the driver determines whether tables or indexes need creation or modification.
Can I use domainx migrations with databases other than MySQL?
Yes, the domainx architecture is explicitly database-agnostic. You can handle database migrations with domainx in Gorig for any storage backend by implementing the DBService interface defined in domainx/service.go (lines 76-80). Whether you are using PostgreSQL, MongoDB, SQLite, or a custom datastore, you simply register your implementation via domainx.RegisterDBService before calling domainx.Start, and the framework routes migration calls to your driver.
What happens if a migration fails during application startup?
If a migration fails, service.Migrate (lines 47-73 in domainx/service.go) returns an error that propagates up through domainx.Start, causing the application to exit with a fatal error. This fail-fast behavior ensures that your application does not start with an incomplete or inconsistent schema. You should wrap critical migrations in transactions within your DBService.Migrate implementation to allow rollback on error, though this capability depends on the specific database driver's transaction support.
Is it possible to run migrations manually instead of automatically on startup?
While domainx is designed to run migrations automatically via domainx.Start, you can trigger them manually by interacting directly with MigrationList and service.Migrate. However, this is not the intended pattern. To disable automatic execution, avoid calling domainx.Start and instead manually iterate over domainx.MigrationList, invoking the migration logic yourself. Note that this requires careful management of the database connection lifecycle, as service.Migrate depends on a fully initialized DBService being registered via domainx.RegisterDBService.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →