FluxaORM v2: Code-Generation-Based Go ORM for MySQL, Redis, and ClickHouse
Guide
GitHub
Guide
GitHub
    • Introduction
    • Registry
    • Data Pools
    • Entities
    • Entity Fields
    • MySQL Indexes
    • Code Generation
    • Engine
    • Context
    • Entity Schema
    • Schema Update
    • CRUD Operations
    • Async Flush
    • Search
    • Redis Search
    • MySQL Queries
    • ClickHouse Queries
    • ClickHouse Schema Management
    • Kafka
    • Debezium CDC
    • Local Cache
    • Context Cache
    • Fake Delete
    • Entity Lifecycle Callbacks
    • Metrics
    • Redis Operations
    • Distributed Lock
    • Queries Log
    • Testing

Code Generation

FluxaORM v2 is built around code generation. You define entities as plain Go structs, register them with a Registry, validate into an Engine, and then call fluxaorm.Generate() to produce fully typed Provider and Entity code. The generated code handles all database scanning, dirty tracking, caching, and query building -- with zero reflection at runtime.

Overview

The code generation workflow has three steps:

  1. Define entity structs with struct tags (orm:"...")
  2. Validate the registry to produce an Engine
  3. Generate typed Go code by calling fluxaorm.Generate(engine, outputDir)

The generated output includes:

Generated typeNaming patternPurpose
ProviderXxxProviderSingleton with table metadata and all query/factory methods
EntityXxxEntityStruct with context, ID, dirty tracking, getters/setters
SQLRowxxxSQLRowFlat struct for reflection-free database scanning
Enumsenums/XxxNameType-safe string enum constants (separate package)

Calling Generate

package main

import (
    "github.com/latolukasz/fluxaorm/v2"
)

func main() {
    registry := fluxaorm.NewRegistry()
    registry.RegisterMySQL("user:password@tcp(localhost:3306)/db", fluxaorm.DefaultPoolCode, nil)
    registry.RegisterRedis("localhost:6379", 0, fluxaorm.DefaultPoolCode, nil)
    registry.RegisterEntity(UserEntity{})
    registry.RegisterEntity(ProductEntity{})
    registry.RegisterEntity(CategoryEntity{})

    engine, err := registry.Validate()
    if err != nil {
        panic(err)
    }

    err = fluxaorm.Generate(engine, "./entities")
    if err != nil {
        panic(err)
    }
}

Generate accepts two arguments:

ParameterTypeDescription
enginefluxaorm.EngineA validated engine containing all registered entity schemas
outputDirectorystringAn existing, writable directory where generated files will be placed

The function will:

  1. Resolve the output directory to an absolute path and verify it exists and is writable.
  2. Find the nearest go.mod to determine the module name (used for the enums import path).
  3. Remove all existing .go files in the output directory (but not subdirectories).
  4. Generate one .go file per entity, named after the table name (e.g., user.go, product.go).
  5. Generate enum type definitions in an enums/ subdirectory if any entity uses enum or set fields.

Warning

Generate deletes all .go files in the output directory before writing new ones. Do not place hand-written Go files in the same directory as generated output.

Tips

Run Generate as part of a dedicated cmd/generate/main.go program or a go generate directive. It should be executed whenever entity definitions change, not at application startup.

Output Structure

Given three entities UserEntity, ProductEntity, and CategoryEntity, the output directory will look like:

entities/
  user.go
  product.go
  category.go
  enums/
    UserStatus.go
    ProductType.go

Each generated file belongs to the package derived from the output directory name (e.g., package entities). The enums/ subdirectory is created only if at least one entity has enum or set fields.

Generated Provider

For each registered entity, the generator creates a Provider -- a package-level singleton variable that holds table metadata and provides all query and factory methods for that entity.

For a struct named UserEntity mapped to table user, the generator produces:

// Private type with table metadata
type userProvider struct {
    tableName         string
    dbCode            string
    redisCode         string
    cacheIndex        uint64
    uuidRedisKeyMutex *sync.Mutex
    // ... additional fields for Redis cache/search if configured
}

// Public singleton -- use this in your application code
var UserProvider = userProvider{
    tableName: "user",
    dbCode:    "default",
    redisCode: "default",
    // ...
}

You never create a Provider yourself. Simply reference the exported variable (e.g., entities.UserProvider) and call methods on it.

Query Methods

Every Provider has the following methods:

GetByID

Fetches a single entity by its primary key. Returns the entity, a boolean indicating whether it was found, and an error.

user, found, err := entities.UserProvider.GetByID(ctx, 42)
if err != nil {
    return err
}
if !found {
    // user with ID 42 does not exist
}
fmt.Println(user.GetName())

Signature:

func (p userProvider) GetByID(ctx fluxaorm.Context, id uint64) (entity *UserEntity, found bool, err error)

The method checks the context cache first, then Redis cache (if configured), and falls back to MySQL.

MustGetByID

A convenience wrapper around GetByID that panics if the entity is not found. The bool return value is removed; errors are still returned normally.

user, err := entities.UserProvider.MustGetByID(ctx, 42)
if err != nil {
    return err
}
// No need to check "found" -- panics if user with ID 42 does not exist
fmt.Println(user.GetName())

Signature:

func (p userProvider) MustGetByID(ctx fluxaorm.Context, id uint64) (entity *UserEntity, err error)

Use MustGetByID when a missing entity indicates a programming error or data inconsistency. It calls GetByID internally and panics with a descriptive message if the entity is not found.

GetByIDs

Fetches multiple entities by their primary keys. Returns a slice containing only the found entities (missing IDs are silently skipped), preserving the order of the input IDs.

users, err := entities.UserProvider.GetByIDs(ctx, 1, 2, 3, 10, 20)
if err != nil {
    return err
}
for _, user := range users {
    fmt.Println(user.GetID(), user.GetName())
}

Signature:

func (p userProvider) GetByIDs(ctx fluxaorm.Context, id ...uint64) ([]*UserEntity, error)

Duplicated IDs in the input are automatically deduplicated. The method uses context cache, Redis cache (if configured), and MySQL in a batched query for any remaining IDs.

New

Creates a new entity instance with an auto-generated UUID. The entity is automatically tracked for flushing.

user := entities.UserProvider.New(ctx)
user.SetName("Alice")
user.SetEmail("alice@example.com")
err := ctx.Flush()

Signature:

func (p userProvider) New(ctx fluxaorm.Context) *UserEntity

UUIDs are generated via Redis INCR, initialized from the current MAX(ID) in MySQL on first use. This guarantees unique, monotonically increasing IDs across all application instances.

NewWithID

Creates a new entity instance with a specific ID. Use this when you need to control the ID value.

user := entities.UserProvider.NewWithID(ctx, 1000)
user.SetName("Bob")
err := ctx.Flush()

Signature:

func (p userProvider) NewWithID(ctx fluxaorm.Context, id uint64) *UserEntity

SearchMany

Queries for entities matching a type-safe query built with fluxaorm.NewQuery().

users, err := entities.UserProvider.SearchMany(ctx,
    fluxaorm.NewQuery().
        Filter(
            entities.UserProvider.Fields.Age.Gt(18),
            entities.UserProvider.Fields.Status.Is("active"),
        ).
        SortByDESC(entities.UserProvider.Fields.CreatedAt).
        Pager(fluxaorm.NewPager(1, 20)),
)
if err != nil {
    return err
}
for _, user := range users {
    fmt.Println(user.GetName())
}

Signature:

func (p userProvider) SearchMany(ctx fluxaorm.Context, query *fluxaorm.DBQuery) ([]*UserEntity, error)

SearchOne

Queries for a single entity matching the query. Adds LIMIT 1 automatically. When the filter conditions match a cached unique index, the cached index lookup is used automatically.

user, found, err := entities.UserProvider.SearchOne(ctx,
    fluxaorm.NewQuery().Filter(
        entities.UserProvider.Fields.Email.Is("alice@example.com"),
    ),
)

Signature:

func (p userProvider) SearchOne(ctx fluxaorm.Context, query *fluxaorm.DBQuery) (*UserEntity, bool, error)

SearchManyWithTotal

Like SearchMany, but also returns the total number of matching rows (before pagination). Useful for building paginated UIs.

users, total, err := entities.UserProvider.SearchManyWithTotal(ctx,
    fluxaorm.NewQuery().
        Filter(entities.UserProvider.Fields.Status.Is("active")).
        Pager(fluxaorm.NewPager(2, 10)),
)
// total = 150 (all matching rows), len(users) = 10 (current page)

Signature:

func (p userProvider) SearchManyWithTotal(ctx fluxaorm.Context, query *fluxaorm.DBQuery) ([]*UserEntity, int, error)

Typed Fields

Every Provider has a Fields struct containing typed field definitions for each column. These are used with fluxaorm.NewQuery() to build type-safe filters and sort clauses:

// Access typed fields on the Provider
entities.UserProvider.Fields.ID        // fluxaorm.UintField
entities.UserProvider.Fields.Name      // fluxaorm.StringField
entities.UserProvider.Fields.Email     // fluxaorm.StringField
entities.UserProvider.Fields.Age       // fluxaorm.UintField
entities.UserProvider.Fields.Status    // fluxaorm.EnumField
entities.UserProvider.Fields.CreatedAt // fluxaorm.TimeField

See the Search page for the full list of field types and their available methods.

Redis Search Methods

If an entity has Redis Search configured, additional methods and fields are generated:

  • SearchManyInRedis(ctx, query) -- search using Redis Search, returns full entities
  • SearchOneInRedis(ctx, query) -- search for a single entity using Redis Search
  • SearchManyInRedisWithTotal(ctx, query) -- search with total count
  • ReindexRedisSearch(ctx) -- rebuild the entire Redis Search index from MySQL data

These methods use *fluxaorm.RedisSearchQuery built with fluxaorm.NewRedisSearchQuery().

The Provider also has a FieldsRedisSearch struct containing typed Redis Search field definitions:

entities.ProductProvider.FieldsRedisSearch.Name   // fluxaorm.RedisSearchTextField
entities.ProductProvider.FieldsRedisSearch.Price  // fluxaorm.RedisSearchNumericField (float64 → float64 params)
entities.ProductProvider.FieldsRedisSearch.Age    // fluxaorm.RedisSearchUintField    (uint32 → uint64 params)
entities.ProductProvider.FieldsRedisSearch.Status // fluxaorm.RedisSearchTagField

See the Redis Search page for details.

Provider Interfaces

All generated Providers implement the fluxaorm.EntityProvider interface, which exposes common metadata:

type EntityProvider interface {
    TableName() string
    DBCode() string
}

Providers with Redis caching (redisCache tag) additionally implement fluxaorm.RedisCacheEntityProvider:

type RedisCacheEntityProvider interface {
    RedisCode() string
    RedisCachePrefix() string
    ClearRedisCache(ctx Context) (int, error)
}

Providers with Redis Search indexing additionally implement fluxaorm.RedisSearchEntityProvider:

type RedisSearchEntityProvider interface {
    ReindexRedisSearch(ctx Context) error
    RedisSearchCode() string
    RedisSearchIndexName() string
    RedisSearchHashPrefix() string
}

These interfaces are independent (they do not embed each other). Use type assertions to check capabilities at runtime:

for _, p := range entities.AllProviders {
    fmt.Println("Table:", p.TableName(), "DB:", p.DBCode())

    if rp, ok := p.(fluxaorm.RedisCacheEntityProvider); ok {
        fmt.Println("  Redis cache prefix:", rp.RedisCachePrefix())
    }
    if sp, ok := p.(fluxaorm.RedisSearchEntityProvider); ok {
        fmt.Println("  Redis search index:", sp.RedisSearchIndexName())
    }
}

Tips

Entities with cached unique indexes (via CachedUniqueIndexes()) but without redisCache do not implement RedisCacheEntityProvider. The Redis cache interface is only for entities that have the full entity-level Redis cache enabled.

AllProviders Registry

The generator also produces a providers.go file containing an AllProviders variable -- a slice of all generated providers typed as fluxaorm.EntityProvider:

var AllProviders = []fluxaorm.EntityProvider{
    &UserProvider,
    &ProductProvider,
    &CategoryProvider,
}

The providers are sorted alphabetically by table name for deterministic output. Use AllProviders to iterate over all entity providers at runtime for tasks like health checks, migrations, or building admin interfaces.

Generated Entity

For each registered entity, the generator creates an Entity struct that wraps the raw database data with context awareness and dirty tracking.

type UserEntity struct {
    ctx                  fluxaorm.Context
    id                   uint64
    new                  bool
    deleted              bool
    originDatabaseValues *userSQLRow
    databaseBind         map[string]any
    // ... additional fields for Redis cache if configured
}

Core Methods

Every generated entity has these methods:

// Returns the entity's primary key
func (e *UserEntity) GetID() uint64

// Marks the entity for deletion on next Flush()
func (e *UserEntity) Delete()

If the entity has a FakeDelete field, Delete() performs a soft delete by setting FakeDelete = true. A separate ForceDelete() method is also generated for hard deletes.

Getters and Setters

For each field in the entity struct, the generator creates typed getter and setter methods. The naming pattern is:

MethodDescription
Get<Field>()Returns the current value (checks dirty map first, then origin)
Set<Field>(value)Sets a new value; automatically tracks the entity for flushing if the value changed

Example for a Name field of type string:

name := user.GetName()      // returns string
user.SetName("New Name")    // sets a new value, marks entity as dirty

Setter dirty tracking: When you call a setter, it compares the new value against the original value. If they are the same, the field is removed from the dirty map (no-op). If they differ, the field is added to the dirty map and the entity is automatically tracked for the next Flush() call.

Field Type Mapping

The generated getters and setters use Go-native types:

Entity field typeGetter return typeSetter parameter type
uint, uint8, ..., uint64uint64uint64
int, int8, ..., int64int64int64
float32, float64float64float64
boolboolbool
stringstringstring
time.Timetime.Timetime.Time
*uint64 (nullable)*uint64*uint64
Enum fieldenums.XxxNameenums.XxxName
Set field[]enums.XxxName...enums.XxxName (variadic)

Reference Fields

For reference (foreign key) fields, the generator creates three methods:

// Returns the raw foreign key value
func (e *ProductEntity) GetCategoryID() uint64

// Sets the raw foreign key value
func (e *ProductEntity) SetCategory(value uint64)

// Loads the referenced entity (calls GetByID on the referenced Provider)
func (e *ProductEntity) GetCategory(ctx fluxaorm.Context) (*CategoryEntity, bool, error)

// Loads the referenced entity, panics if not found
func (e *ProductEntity) MustGetCategory(ctx fluxaorm.Context) (*CategoryEntity, error)

The MustGet<Reference> method is a convenience wrapper around Get<Reference>. It removes the bool return value and panics if the referenced entity is not found. Errors are still returned normally. This works the same way for both required and optional references.

For optional (non-required) references, the ID getter also returns uint64, with 0 representing NULL (no reference set). The setter accepts uint64 and treats 0 as NULL.

Generated SQLRow

The SQLRow struct is a flat, unexported type used internally for reflection-free database scanning. Each field is named F0, F1, F2, etc., matching the column order.

type userSQLRow struct {
    F0 uint64         // ID
    F1 string         // Name
    F2 sql.NullString // Email (nullable)
    F3 time.Time      // CreatedAt
    // ...
}

You do not interact with SQLRow directly. It is used by the generated Provider methods to scan query results and by the Entity methods to read original values.

Generated Enums

When an entity field uses the enum or set struct tag, the generator creates type-safe enum definitions in the enums/ subdirectory.

For example, given an entity field:

type UserEntity struct {
    ID     uint64
    Status string `orm:"enum=active,inactive,banned;required"`
}

The generator creates enums/UserStatus.go:

package enums

type UserStatus string

var UserStatusList = struct {
    Active   UserStatus
    Inactive UserStatus
    Banned   UserStatus
}{
    Active:   "active",
    Inactive: "inactive",
    Banned:   "banned",
}

Use enum values in your application code:

import "your/module/entities/enums"

user := entities.UserProvider.New(ctx)
user.SetStatus(enums.UserStatusList.Active)

status := user.GetStatus() // returns enums.UserStatus
if status == enums.UserStatusList.Banned {
    // handle banned user
}

Practical Workflow

Here is a complete example showing the typical development workflow with FluxaORM code generation.

Step 1: Define Entities

package main

import "time"

type UserEntity struct {
    ID        uint64
    Name      string    `orm:"required;length=200"`
    Email     string    `orm:"required;length=255"`
    Age       uint8
    Status    string    `orm:"enum=active,inactive,banned;required"`
    CreatedAt time.Time `orm:"time"`
}

func (e UserEntity) UniqueIndexes() map[string][]string {
    return map[string][]string{"Email": {"Email"}}
}

type ProductEntity struct {
    ID          uint64
    Name        string `orm:"required;length=200"`
    Category    *CategoryEntity
    Price       float64 `orm:"decimal=10,2"`
}

type CategoryEntity struct {
    ID   uint64
    Name string `orm:"required;length=100"`
}

Step 2: Generate Code

Create a generation program (e.g., cmd/generate/main.go):

package main

import (
    "github.com/latolukasz/fluxaorm/v2"
)

func main() {
    registry := fluxaorm.NewRegistry()
    registry.RegisterMySQL("user:password@tcp(localhost:3306)/db", fluxaorm.DefaultPoolCode, nil)
    registry.RegisterRedis("localhost:6379", 0, fluxaorm.DefaultPoolCode, nil)
    registry.RegisterEntity(UserEntity{})
    registry.RegisterEntity(ProductEntity{})
    registry.RegisterEntity(CategoryEntity{})

    engine, err := registry.Validate()
    if err != nil {
        panic(err)
    }

    err = fluxaorm.Generate(engine, "./entities")
    if err != nil {
        panic(err)
    }
}

Run it:

go run cmd/generate/main.go

Step 3: Use Generated Code

package main

import (
    "context"
    "fmt"

    "github.com/latolukasz/fluxaorm/v2"
    "your/module/entities"
    "your/module/entities/enums"
)

func main() {
    // ... setup registry and engine (same as above)
    ctx := engine.NewContext(context.Background())

    // Create a new user
    user := entities.UserProvider.New(ctx)
    user.SetName("Alice")
    user.SetEmail("alice@example.com")
    user.SetAge(30)
    user.SetStatus(enums.UserStatusList.Active)

    // Create a category and product
    category := entities.CategoryProvider.New(ctx)
    category.SetName("Electronics")

    product := entities.ProductProvider.New(ctx)
    product.SetName("Laptop")
    product.SetCategoryID(category.GetID())
    product.SetPrice(999.99)

    // Persist all changes in a single flush
    err := ctx.Flush()
    if err != nil {
        panic(err)
    }

    // Query entities
    foundUser, exists, err := entities.UserProvider.GetByID(ctx, user.GetID())
    if err != nil {
        panic(err)
    }
    if exists {
        fmt.Println("Found user:", foundUser.GetName())
    }

    // Search with type-safe query builder
    users, err := entities.UserProvider.SearchMany(ctx,
        fluxaorm.NewQuery().
            Filter(entities.UserProvider.Fields.Age.Gte(18)).
            SortByASC(entities.UserProvider.Fields.Name).
            Pager(fluxaorm.NewPager(1, 10)),
    )
    if err != nil {
        panic(err)
    }
    for _, u := range users {
        fmt.Println(u.GetName(), u.GetEmail())
    }

    // Search by unique index (auto-detected via SearchOne)
    userByEmail, found, err := entities.UserProvider.SearchOne(ctx,
        fluxaorm.NewQuery().Filter(
            entities.UserProvider.Fields.Email.Is("alice@example.com"),
        ),
    )
    if err != nil {
        panic(err)
    }
    if found {
        fmt.Println("Found by email:", userByEmail.GetName())
    }

    // Update an entity
    foundUser.SetName("Alice Updated")
    err = ctx.Flush()
    if err != nil {
        panic(err)
    }

    // Delete an entity
    foundUser.Delete()
    err = ctx.Flush()
    if err != nil {
        panic(err)
    }
}
Edit this page
Last Updated: 4/1/26, 8:37 AM
Prev
MySQL Indexes
Next
Engine