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

Engine

The Engine is the validated, read-only result of a Registry. It owns the connection pools, the entity schemas and the messaging topology, and it is the factory for the Context objects every data operation needs.

Creating the Engine

An Engine is obtained from registry.Validate() (or registry.ValidateForCodeGen() for code generation without network access):

package main

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

type UserEntity struct {
    ID   uint64
    Name string `orm:"required"`
}

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

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

Tips

Create the Registry and call Validate() exactly once, when the application starts - for an HTTP server, before http.ListenAndServe. Share the resulting Engine across all goroutines.

Engine Interface

type Engine interface {
    // Creates a Context bound to the given context.Context
    NewContext(parent context.Context) Context

    // Connection pools by code; nil when the code is unknown
    DB(code string) DB
    Clickhouse(code string) Clickhouse
    Nats(code string) Nats
    Redis(code string) RedisCache

    // Metadata about all pools and registry options
    Registry() EngineRegistry

    // Runtime options set with EngineSetter.SetOption
    Option(key string) any

    // Snowflake ID generator
    NextID() uint64
    SetNodeID(node int64)
}

type EngineSetter interface {
    SetOption(key string, value any)
}

const DefaultPoolCode = "default"

Creating a Context

The main purpose of the Engine is to create Context instances. A Context wraps a standard context.Context, so deadlines, cancellation and request-scoped values from an HTTP handler or gRPC interceptor flow into every FluxaORM operation:

import "context"

ctx := engine.NewContext(context.Background())

func handleRequest(w http.ResponseWriter, r *http.Request) {
    ctx := engine.NewContext(r.Context())
    // use ctx for all database operations in this request
}

Create one Context per request or unit of work. See Context.

Accessing Pools

Each getter returns the pool registered under the given code, or nil when no such pool exists - there is no error return, so guard against nil when the code comes from configuration.

db := engine.DB(fluxaorm.DefaultPoolCode)          // MySQL pool
redisPool := engine.Redis(fluxaorm.DefaultPoolCode) // Redis pool
ch := engine.Clickhouse("analytics")                // ClickHouse pool
natsPool := engine.Nats(fluxaorm.DefaultPoolCode)   // NATS pool
GetterReturnsNotes
DB(code)DBNon-transactional MySQL handle. Inside a transaction use ctx.DB(code) instead (see Transactions). Raw queries: MySQL Queries.
Redis(code)RedisCacheRedis commands, pipelines and locks: Redis Operations, Distributed Lock.
Clickhouse(code)ClickhouseClickHouse Queries.
Nats(code)NatsConnects lazily on first use; call Ping() to connect eagerly and Close() on shutdown. See NATS.

Engine Registry

engine.Registry() returns an EngineRegistry with the complete pool maps and the options set on the Registry:

type EngineRegistry interface {
    DBPools() map[string]DB
    ClickhousePools() map[string]Clickhouse
    NatsPools() map[string]Nats
    RedisPools() map[string]RedisCache
    Option(key string) any
}
for code := range engine.Registry().DBPools() {
    fmt.Println("MySQL pool:", code)
}
for code := range engine.Registry().RedisPools() {
    fmt.Println("Redis pool:", code)
}
for code := range engine.Registry().ClickhousePools() {
    fmt.Println("ClickHouse pool:", code)
}
for code := range engine.Registry().NatsPools() {
    fmt.Println("NATS pool:", code)
}

The maps are the engine's live internal maps; treat them as read-only.

Options

There are two independent key-value stores:

Set withRead withPurpose
registry.SetOption(key, value) before Validate()engine.Registry().Option(key)Static configuration decided at startup
engine.(fluxaorm.EngineSetter).SetOption(key, value)engine.Option(key)Values attached at runtime
registry.SetOption("app_name", "my-service")
engine, _ := registry.Validate()
engine.Registry().Option("app_name") // "my-service"
engine.Option("app_name")            // nil - different store

engine.(fluxaorm.EngineSetter).SetOption("region", "eu-west-1")
engine.Option("region")              // "eu-west-1"

EngineSetter.SetOption writes to a plain map and is not safe to call concurrently with readers; set runtime options during startup.

ID Generation

Every new entity receives its primary key from the engine's in-process snowflake generator; NextID() exposes the same generator for your own use:

id := engine.NextID()

An ID is a positive 63-bit value composed of:

BitsContent
41milliseconds since 2024-01-01T00:00:00Z
11node ID (0-2047)
11per-millisecond sequence (0-2047)

NextID() never touches the network and never fails. IDs from one process are strictly increasing: if the clock moves backwards the generator keeps issuing from the last timestamp, and when the sequence for a millisecond is exhausted it rolls forward to the next one.

Uniqueness across processes relies on each running process having its own node ID. The engine starts with node 0; assign a distinct node per process right after Validate():

engine.SetNodeID(nodeID) // e.g. from an environment variable, the pod ordinal, or a lease

SetNodeID masks the value to 11 bits (node & 2047) instead of failing, so two processes whose IDs differ by 2048 would collide - keep node IDs in 0-2047.

Caution

Two processes with the same node ID can generate the same primary key within the same millisecond. Always give every replica of your application a unique node ID.

Lifecycle Handlers

Generated provider code registers after-write hooks on the engine through three package-level functions:

func RegisterAfterInsertHandler(engine Engine, cacheIndex string, handler func(Context, Entity) error)
func RegisterAfterUpdateHandler(engine Engine, cacheIndex string, handler func(Context, Entity, map[string]any) error)
func RegisterAfterDeleteHandler(engine Engine, cacheIndex string, handler func(Context, Entity) error)

One handler is stored per cacheIndex (the entity's package-qualified type name); a later registration replaces the earlier one. Prefer the typed OnAfterInsert/OnAfterUpdate/OnAfterDelete methods of the generated providers, which call these functions for you - see Lifecycle Callbacks.

Immutability

After Validate() the set of pools, entities, streams, consumers and tasks is fixed: there is no way to register more on an Engine, and no Close() - pools live as long as the process (close NATS connections explicitly with engine.Nats(code).Close() if needed). The only mutable state on an Engine is:

  • the node ID (SetNodeID),
  • runtime options (EngineSetter.SetOption),
  • lifecycle handlers (RegisterAfter*Handler),
  • mock database clients used in tests (SetMockDBClient, see Testing).

All of these are meant to be set during startup, before the engine is shared between goroutines.

Edit this page
Last Updated: 9/21/26, 9:14 AM
Prev
Code Generation
Next
Context