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:

  1. Registration: During package initialization, domainx.AutoMigrate (lines 61-67 in domainx/api.go) appends migration entries to MigrationList.
  2. Startup: domainx.Start invokes service.Start, which initializes all database connections.
  3. Execution: After a 1-second grace period (lines 29-34 in domainx/service.go), the system iterates through MigrationList and calls service.Migrate for each entry.
  4. Resolution: service.Migrate (lines 47-73) resolves the appropriate DBService implementation 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:

  1. Invokes the migration's DBFunc to obtain the ConTable instance
  2. Extracts the connection via GetCon()
  3. Dispatches to the appropriate DBService.Migrate implementation based on con.GetConType()

Summary

Handling database migrations with domainx in Gorig follows a declarative, service-oriented pattern:

  • Define models by embedding domainx.Con and implementing the ConTable interface with a TableName() method
  • Register migrations using domainx.AutoMigrate in package init() functions, optionally specifying indexes via CtIdx
  • Implement drivers by satisfying the DBService interface, specifically implementing the Migrate method to handle schema creation
  • Execute automatically by calling domainx.Start, which applies all pending migrations from MigrationList after 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →