FluxaORM v2: Code-Generation-Based Go ORM for MySQL, Redis, ClickHouse and NATS JetStream
Guide
GitHub
Guide
GitHub
    • Introduction
    • Registry
    • Data Pools
    • Entities
    • Entity Fields
    • MySQL Indexes
    • Code Generation
    • Engine
    • Context
    • Schema Update
    • CRUD Operations
    • Transactions
    • Search
    • Redis Search
    • MySQL Queries
    • ClickHouse Queries
    • ClickHouse Schema
    • NATS and JetStream
    • Entity Change Events (CDC)
    • Consumers
    • Tasks and Job Runs
    • Transactional Outbox
    • Context Cache
    • Redis Cache
    • Fake Delete
    • Lifecycle Callbacks
    • Metrics
    • Redis Operations
    • Distributed Lock
    • Queries Log
    • Testing

Code Generation

FluxaORM uses no reflection at runtime. You describe entities as plain Go structs (see Entities), register them in a Registry, and run fluxaorm.Generate(). The generator emits one Go file per entity containing a typed Provider (loading, searching, factory and metadata methods) and a typed Entity (getters, setters, dirty tracking and the SQL it needs to persist itself), plus a shared enums package. This page is a reference for what is generated and how to call it.

Running the generator

func Generate(engine Engine, outputDirectory string) error

Generate needs a validated Engine. Use registry.ValidateForCodeGen() rather than Validate(): it builds the entity schemas without opening a single connection to MySQL, Redis or NATS, so the generator can run on a machine that has no access to the databases (a developer laptop, CI). Pools still have to be registered so that tags such as orm:"mysql=logs" or orm:"redisCache" can be resolved, but the addresses are never dialled.

Put the call in a small program and run it whenever an entity struct changes:

// cmd/generate/main.go
package main

import (
    "os"

    "github.com/latolukasz/fluxaorm/v2"

    "myapp/model"
)

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

    engine, err := registry.ValidateForCodeGen()
    if err != nil {
        panic(err)
    }
    if err = os.MkdirAll("entities", 0o755); err != nil {
        panic(err)
    }
    if err = fluxaorm.Generate(engine, "entities"); err != nil {
        panic(err)
    }
}
go run ./cmd/generate

What Generate does, in order:

  1. Checks the output directory: it must be non-empty (output directory is empty), exist (output directory does not exist: <path>), be a directory and be writable.
  2. Walks up from the output directory until it finds a go.mod and reads its module line. The import path of the generated enums package is <module>/<relative path of the output dir>/enums, so the output directory must live inside a Go module.
  3. Deletes every regular file in the output directory, whatever its extension. Sub-directories (such as enums/) are kept: the file of every enum still in use is rewritten, but a file left over from an enum that no longer exists is not removed. Never put hand-written files in the output directory.
  4. Writes one file per registered entity, then providers.go, then the consumer files when consumers or tasks are registered.
  5. Runs every file through go/format.

The package name of the generated code is the base name of the output directory (entities in the example above). Commit the generated files together with the entity structs that produced them.

Upgrading FluxaORM

Regenerate all entities after updating the ORM dependency, even when your model structs have not changed. The generator and runtime must agree on the entity persistence protocol.

The transaction save fix requires generated snapshot and rollback support, including PrivateSnapshot, PrivateRollback and PrivateIsSnapshot. Older generated entities can still compile, but Save returns fluxaorm.ErrEntityNeedsRegeneration rather than using an entity without this support. Run your generator with the updated dependency, then rebuild and test the application. Save, Transaction and the public setters keep their existing signatures.

The GetByID and GetByIDs allocation improvements are emitted into generated providers. Rerun your application generator with the updated ORM dependency to adopt them; changing the dependency alone does not rewrite existing providers. These optimizations preserve method signatures, cache behavior, and missing-row behavior, as well as input order and duplicate handling in GetByIDs. Newly generated readers require the updated ORM runtime for numeric scan adapters and Redis command batching; database/sql conversions, NULL handling, overflow errors, and the Redis row-cache format are preserved.

Output layout

FileContent
<TableName>.goOne file per entity. The file name is the table name verbatim: UserEntity.go for a struct UserEntity, users.go when the struct carries orm:"table=users".
providers.govar AllProviders = []fluxaorm.EntityProvider{...}, pointers to every provider sorted by table name.
consumers.go, <consumer>_consumer.goOnly when consumers or tasks are registered. See Consumers and Tasks.
enums/<EnumName>.goPackage enums, one file per enum name (see Enums).

Naming rules

  • The table name is the struct type name unless the ID field carries orm:"table=...".
  • The entity type is the table name with its first letter capitalised and underscores removed, capitalising each segment: UserEntity stays UserEntity, a table user_accounts becomes UserAccounts.
  • The provider variable is <Entity>Provider (UserEntityProvider). Its type, the fields struct and the SQL row struct are unexported (userEntityProvider, userEntityFields, userEntitySQLRow).
  • The cache index stored in the provider is the fully qualified Go type name of the source struct (model.UserEntity). It is assigned at validation time and used as the key of the context cache and of the lifecycle handlers.

The examples on this and the following pages use these two structs, defined in a package model and generated into a package entities:

package model

import (
    "time"

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

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

func (e CategoryEntity) UniqueIndexes() [][]string {
    return [][]string{{"Code"}}
}

func (e CategoryEntity) CachedUniqueIndexes() [][]string {
    return [][]string{{"Code"}}
}

type UserEntity struct {
    ID        uint64 `orm:"redisCache"`
    Name      string `orm:"required;length=100"`
    Email     string `orm:"length=255"`
    Age       *uint8
    Status    string `orm:"enum=active,blocked;required;enumName=UserStatus"`
    Category  fluxaorm.Reference[CategoryEntity] `orm:"required"`
    CreatedAt time.Time
    UpdatedAt time.Time
}

Anatomy of a generated file

The excerpt below is what entities/UserEntity.go looks like (the hex values are computed from the table and column names and differ for every entity; method bodies are omitted).

// Code generated by fluxaorm; DO NOT EDIT.

package entities

type userEntityFields struct {
    ID        fluxaorm.UintField
    Category  fluxaorm.ReferenceField
    CreatedAt fluxaorm.TimeField
    UpdatedAt fluxaorm.TimeField
    Name      fluxaorm.StringField
    Email     fluxaorm.NullableStringField
    Age       fluxaorm.NullableUintField
    Status    fluxaorm.EnumField
}

type userEntityProvider struct {
    tableName        string
    dbCode           string
    redisCode        string
    cacheIndex       string
    redisCachePrefix string
    redisCacheStamp  string
    redisCacheTTL    int
    Fields           userEntityFields
}

var UserEntityProvider = userEntityProvider{
    tableName:        "UserEntity",
    dbCode:           "default",
    redisCode:        "default",
    cacheIndex:       "model.UserEntity",
    redisCachePrefix: "e11dd246:",
    redisCacheStamp:  "e69f360206e0533c",
    redisCacheTTL:    0,
    Fields: userEntityFields{
        ID:       fluxaorm.UintField{Column: "ID"},
        Category: fluxaorm.ReferenceField{Column: "Category"},
        // ...
    },
}

func (p userEntityProvider) TableName() string { return p.tableName }
func (p userEntityProvider) DBCode() string    { return p.dbCode }

func (p userEntityProvider) RedisCode() string        { return p.redisCode }
func (p userEntityProvider) RedisCachePrefix() string { return p.redisCachePrefix }
func (p userEntityProvider) ClearRedisCache(ctx fluxaorm.Context) (int, error)

type userEntitySQLRow struct {
    F0 uint64
    F1 uint64
    F2 time.Time
    F3 time.Time
    F4 string
    F5 sql.NullString
    F6 sql.NullInt64
    F7 string
}

func (p userEntityProvider) OnAfterInsert(engine fluxaorm.Engine, handler func(ctx fluxaorm.Context, entity *UserEntity) error)
func (p userEntityProvider) OnAfterUpdate(engine fluxaorm.Engine, handler func(ctx fluxaorm.Context, entity *UserEntity, changes map[string]any) error)
func (p userEntityProvider) OnAfterDelete(engine fluxaorm.Engine, handler func(ctx fluxaorm.Context, entity *UserEntity) error)

func RegisterUserEntityBeforeInsert(cb func(entity *UserEntity))
func RegisterUserEntityBeforeUpdate(cb func(entity *UserEntity))
func RegisterUserEntityBeforeDelete(cb func(entity *UserEntity))

func (p userEntityProvider) GetByID(ctx fluxaorm.Context, id uint64) (entity *UserEntity, found bool, err error)
func (p userEntityProvider) MustGetByID(ctx fluxaorm.Context, id uint64) (entity *UserEntity, err error)
func (p userEntityProvider) GetByIDs(ctx fluxaorm.Context, id ...uint64) ([]*UserEntity, error)
func (p userEntityProvider) New(ctx fluxaorm.Context) *UserEntity
func (p userEntityProvider) NewWithID(ctx fluxaorm.Context, id uint64) *UserEntity
func (p userEntityProvider) SearchOne(ctx fluxaorm.Context, query *fluxaorm.DBQuery) (*UserEntity, bool, error)
func (p userEntityProvider) SearchMany(ctx fluxaorm.Context, query *fluxaorm.DBQuery) ([]*UserEntity, error)
func (p userEntityProvider) SearchManyWithTotal(ctx fluxaorm.Context, query *fluxaorm.DBQuery) ([]*UserEntity, int, error)
func (p userEntityProvider) Count(ctx fluxaorm.Context, query *fluxaorm.DBQuery) (int, error)

type UserEntity struct {
    ctx                  fluxaorm.Context
    id                   uint64
    new                  bool
    deleted              bool
    removed              bool
    snapshot             bool
    originDatabaseValues *userEntitySQLRow
    databaseBind         map[string]any
    originRedisValues    []string
    flushType            uint8
    flushChanges         map[string]any
}

func (e *UserEntity) GetID() uint64

func (e *UserEntity) GetName() string
func (e *UserEntity) SetName(value string) *UserEntity
func (e *UserEntity) GetEmail() string
func (e *UserEntity) SetEmail(value string) *UserEntity
func (e *UserEntity) GetAge() *uint64
func (e *UserEntity) SetAge(value *uint64) *UserEntity
func (e *UserEntity) GetStatus() enums.UserStatus
func (e *UserEntity) SetStatus(value enums.UserStatus) *UserEntity
func (e *UserEntity) GetCategoryID() uint64
func (e *UserEntity) SetCategory(value uint64) *UserEntity
func (e *UserEntity) GetCategory(ctx fluxaorm.Context) (reference *CategoryEntity, found bool, err error)
func (e *UserEntity) MustGetCategory(ctx fluxaorm.Context) (reference *CategoryEntity, err error)
func (e *UserEntity) GetCreatedAt() time.Time
func (e *UserEntity) SetCreatedAt(value time.Time) *UserEntity
func (e *UserEntity) GetUpdatedAt() time.Time
func (e *UserEntity) SetUpdatedAt(value time.Time) *UserEntity

The redisCachePrefix, redisCacheStamp and redisCacheTTL provider fields and the entity's originRedisValues exist only for entities tagged orm:"redisCache"; an entity that only declares CachedUniqueIndexes() gets redisCachePrefix alone. Entities with searchable fields additionally carry redisSearchCode, redisSearchIndex, redisSearchPrefix and a FieldsRedisSearch descriptor struct.

Methods whose name starts with Private (PrivateFlush, PrivateFlushed, PrivateSnapshot, PrivateRollback, PrivateAdvance, PrivateIsSnapshot, PrivateIsDeleted, PrivateDatabasePool, PrivateReload, PrivateDelete, PrivateForceDelete (the latter only on FakeDelete entities), PrivateFlushEvent, PrivateGetDatabaseBind, PrivateCacheIndex, PrivateIsNew, PrivateContext) implement the fluxaorm.Entity interface or internal capabilities used by ctx.Save, ctx.Delete, ctx.Reload and transactions. They are exported only because the ORM lives in another package; never call them yourself.

The Provider

Field descriptors

Provider.Fields holds one typed descriptor per column. Descriptors build the conditions passed to fluxaorm.NewQuery().Filter(...) and the sort arguments of SortByASC/SortByDESC:

users, err := entities.UserEntityProvider.SearchMany(ctx,
    fluxaorm.NewQuery().
        Filter(
            entities.UserEntityProvider.Fields.Category.Eq(categoryID),
            entities.UserEntityProvider.Fields.Status.Is(enums.UserStatusList.Active),
        ).
        SortByASC(entities.UserEntityProvider.Fields.Name).
        Pager(fluxaorm.NewPager(1, 20)),
)
Go fieldDescriptor
uint* (including ID)fluxaorm.UintField
*uint*fluxaorm.NullableUintField
int* / *int*fluxaorm.IntField / fluxaorm.NullableIntField
float* / *float*fluxaorm.FloatField / fluxaorm.NullableFloatField
bool / *boolfluxaorm.BoolField / fluxaorm.NullableBoolField
time.Time / *time.Timefluxaorm.TimeField / fluxaorm.NullableTimeField
string with requiredfluxaorm.StringField
string without requiredfluxaorm.NullableStringField
string with enum= and requiredfluxaorm.EnumField
string with enum= without requiredfluxaorm.NullableEnumField
fluxaorm.Reference[T] with requiredfluxaorm.ReferenceField
fluxaorm.Reference[T] without requiredfluxaorm.NullableUintField

FakeDelete, []uint8, set= fields, fluxaorm.References[T] and JSON struct fields have no descriptor: they cannot be used in Filter. Fields of embedded or nested structs are flattened with the nested field name as prefix (TestSubSize for TestSub.Size), anonymous embedding adds no prefix. The available condition methods are documented in Search.

Metadata methods and interfaces

Every provider implements fluxaorm.EntityProvider:

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

Entities with orm:"redisCache" or with CachedUniqueIndexes() additionally implement fluxaorm.RedisCacheEntityProvider (an entity that only caches unique index lookups still needs a way to clear those keys):

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

Entities with searchable fields implement fluxaorm.RedisSearchEntityProvider:

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

providers.go collects every provider so that tooling can iterate over them and pick the capabilities it needs:

for _, provider := range entities.AllProviders {
    if cached, ok := provider.(fluxaorm.RedisCacheEntityProvider); ok {
        deleted, err := cached.ClearRedisCache(ctx)
        // ...
    }
}

Loading, factory and search methods

MethodNotes
GetByID(ctx, id) (*E, bool, error)Context cache, then Redis row cache, then MySQL. See CRUD.
MustGetByID(ctx, id) (*E, error)Like GetByID but panics with <Entity> with id <id> not found when the row does not exist.
GetByIDs(ctx, id ...uint64) ([]*E, error)Input order preserved, duplicates collapsed, missing ids skipped.
New(ctx) *ENew entity with a snowflake ID from ctx.Engine().NextID(); never fails.
NewWithID(ctx, id) *ENew entity with a caller-chosen ID.
SearchOne(ctx, *DBQuery) (*E, bool, error)Uses the cached unique index fast path when the filter matches one exactly.
SearchMany(ctx, *DBQuery) ([]*E, error)
SearchManyWithTotal(ctx, *DBQuery) ([]*E, int, error)
Count(ctx, *DBQuery) (int, error)
SearchOneInRedis(ctx, *RedisSearchQuery) (*E, bool, error)Only with searchable fields.
SearchManyInRedis(ctx, *RedisSearchQuery) ([]*E, error)Only with searchable fields.
SearchManyInRedisWithTotal(ctx, *RedisSearchQuery) ([]*E, int, error)Only with searchable fields.
ReindexRedisSearch(ctx) errorOnly with searchable fields.

GetByID, GetByIDs, New, NewWithID are described in CRUD; the Search* and Count methods in Search; the *InRedis methods and ReindexRedisSearch in Redis Search. There is no generated Delete on the provider and no GetBy<Index> methods: deleting goes through ctx.Delete, unique index lookups through SearchOne.

Lifecycle registration

OnAfterInsert, OnAfterUpdate and OnAfterDelete register one post-commit handler per event on the Engine. The package-level Register<Entity>BeforeInsert, Register<Entity>BeforeUpdate and Register<Entity>BeforeDelete functions register synchronous callbacks that run before the SQL statement is built. Both families are documented in Lifecycle Callbacks.

The Entity

An entity remembers the Context that created or loaded it, its ID, its latest loaded or successfully saved values (originDatabaseValues / originRedisValues) and the columns changed since (databaseBind). A successful Save advances that baseline immediately after SQL execution, even before a transaction commits. The ORM keeps separate snapshots for deferred events and rollback; see Transactions. The entity can only be saved on its own context; see CRUD for the write rules.

Getters and setters

Every setter returns the entity, so calls chain:

user := entities.UserEntityProvider.New(ctx)
user.SetName("Alice").
    SetEmail("alice@example.com").
    SetStatus(enums.UserStatusList.Active).
    SetCategory(category.GetID())
Go fieldGetterSetter
uint*Get<F>() uint64Set<F>(value uint64)
int*Get<F>() int64Set<F>(value int64)
float*Get<F>() float64Set<F>(value float64)
boolGet<F>() boolSet<F>(value bool)
time.TimeGet<F>() time.TimeSet<F>(value time.Time) — truncated to the second (orm:"time") or to the day (date)
string with requiredGet<F>() stringSet<F>(value string)
string without requiredGet<F>() string — "" for NULLSet<F>(value string) — "" stores NULL
*uint*Get<F>() *uint64Set<F>(value *uint64) — nil stores NULL
*int*Get<F>() *int64Set<F>(value *int64)
*float*Get<F>() *float64Set<F>(value *float64)
*boolGet<F>() *boolSet<F>(value *bool)
*time.TimeGet<F>() *time.TimeSet<F>(value *time.Time)
[]uint8Get<F>() []uint8 — nil for NULLSet<F>(value []uint8)
enum with requiredGet<F>() enums.XSet<F>(value enums.X)
enum without requiredGet<F>() *enums.XSet<F>(value *enums.X)
set with requiredGet<F>() []enums.XSet<F>(value ...enums.X) — stored sorted, comma-joined
set without requiredGet<F>() []enums.X — nil for NULLSet<F>(value ...enums.X) — no arguments stores NULL
JSON struct *pkg.TGet<F>() *pkg.TSet<F>(value *pkg.T) — nil stores NULL
fluxaorm.Reference[T]Get<F>ID() uint64 (0 for NULL), Get<F>(ctx) (*T, bool, error), MustGet<F>(ctx) (*T, error)Set<F>(value uint64) — 0 stores NULL on an optional reference
fluxaorm.References[T]Get<F>IDs() []uint64, Get<F>(ctx) ([]*T, error)Set<F>IDs(ids []uint64) — empty stores []; nil stores NULL on an optional field

Narrow Go integers widen in the API: a uint32 column has Get<F>() uint64, a *uint8 column has Get<F>() *uint64. The ID field has only GetID().

Reference getters load lazily through the referenced provider: Get<F>(ctx) returns nil, false, nil when the ID is 0, otherwise <Ref>Provider.GetByID(ctx, id). MustGet<F>(ctx) panics with <F> not found in <Entity> when the referenced row is missing. Get<F>(ctx) on a References[T] field calls <Ref>Provider.GetByIDs(ctx, ids...).

Change events

Entities tagged orm:"cdc" also get a <Entity>DirtyEvent type (an alias of fluxaorm.DirtyEvent[map[string]any]) and the publisher registration that emits one event per committed write. See Entity Events.

Enums

Every enum= or set= field produces a file in the enums package. The type name is the enumName= tag value; without it the generator derives one from the entity and field name. Several fields — in the same or in different entities — may share one enum by giving them the same enumName; a field may even reference an enum that is defined elsewhere with orm:"enumName=UserStatus" alone.

// Code generated by fluxaorm; DO NOT EDIT.

package enums

type UserStatus string

var UserStatusList = struct {
    Active  UserStatus
    Blocked UserStatus
}{
    Active:  "active",
    Blocked: "blocked",
}

func (e UserStatus) Valid() bool {
    switch e {
    case "active", "blocked":
        return true
    }
    return false
}

func (e UserStatus) Values() []UserStatus {
    return []UserStatus{"active", "blocked"}
}

The constant names are the values with the first letter of every _-separated segment capitalised (in_review becomes InReview). A required enum or set field of a freshly created entity defaults to the first declared value, so entities.UserEntityProvider.New(ctx).GetStatus() is enums.UserStatusList.Active.

Edit this page
Last Updated: 9/21/26, 9:14 AM
Prev
MySQL Indexes
Next
Engine