Hanzo

Hanzo ORM

Generics-based ORM for Go with type-safe Model[T], auto-registration, auto-serialization, and multi-backend support.

Hanzo ORM

Hanzo ORM is a generics-based object-relational mapper for Go that eliminates model boilerplate through a single Model[T] mixin. Define your struct, register it, and get type-safe CRUD, queries, lifecycle hooks, auto-serialization, and caching -- all with zero code generation.

Module: github.com/hanzoai/orm Version: v0.3.0 Go: 1.22+ License: MIT

Install

go get github.com/hanzoai/orm@latest

Quick Start

Define a Model

package main

import "github.com/hanzoai/orm"

type User struct {
    orm.Model[User]
    Name   string `json:"name"`
    Email  string `json:"email"`
    Status string `json:"status" orm:"default:active"`
}

func init() {
    orm.Register[User]("user")
}

Embedding orm.Model[User] gives your struct an auto-generated ID, kind name, and all CRUD methods. The init() call registers metadata at startup -- no code generation step.

Connect to a Backend

import ormdb "github.com/hanzoai/orm/db"

// SQLite (embedded, zero config)
db, err := orm.OpenSQLite(&ormdb.SQLiteDBConfig{
    Path:   "data/app.db",
    Config: ormdb.SQLiteConfig{BusyTimeout: 5000, JournalMode: "WAL"},
})

// Or ZAP binary protocol (PostgreSQL, MongoDB, Redis, ClickHouse)
db, err := orm.OpenZap(&ormdb.ZapConfig{
    Addr:    "localhost:9651",
    Backend: ormdb.ZapSQL,
})
if err != nil {
    log.Fatal(err)
}

Create

user := orm.New[User](db)
user.Name = "Alice"
user.Email = "alice@example.com"
// Status defaults to "active" via orm:"default:active"

if err := user.Create(); err != nil {
    log.Fatal(err)
}

fmt.Println(user.Id()) // unique nanosecond-precision ID

Read

got, err := orm.Get[User](db, user.Id())
if err != nil {
    log.Fatal(err)
}
fmt.Println(got.Name) // "Alice"

Update

got.Name = "Alice Smith"
if err := got.Update(); err != nil {
    log.Fatal(err)
}

Delete

if err := got.Delete(); err != nil {
    log.Fatal(err)
}

Query

q := orm.TypedQuery[User](db)
active, err := q.Filter("Status=", "active").Get()
if err != nil {
    log.Fatal(err)
}
for _, u := range active {
    fmt.Println(u.Name, u.Email)
}

Architecture

┌─────────────────────────────────────────────────────┐
│  Your Application                                    │
│  ┌────────────┐  ┌────────────┐  ┌──────────────┐  │
│  │ Model[User]│  │Model[Order]│  │Model[Product]│  │
│  └─────┬──────┘  └─────┬──────┘  └──────┬───────┘  │
│        │               │                │           │
│        ▼               ▼                ▼           │
│  ┌─────────────────────────────────────────────┐    │
│  │           orm.DB Interface                   │    │
│  │  New[T] · Get[T] · TypedQuery[T]           │    │
│  │  Register · Hooks · Defaults · Serialize    │    │
│  └─────────────────────┬───────────────────────┘    │
│                        │                             │
│  ┌─────────────────────┼───────────────────────┐    │
│  │          Cache Layer (optional)              │    │
│  │  Memory LRU · Redis/Valkey · None           │    │
│  └─────────────────────┬───────────────────────┘    │
│                        │                             │
│  ┌─────────┬───────────┼──────────┬────────────┐    │
│  │ SQLite  │ ZAP Binary Protocol  │  Custom    │    │
│  │ (WAL)   │ PostgreSQL│ MongoDB  │  Driver    │    │
│  │         │ Redis     │ClickHouse│            │    │
│  └─────────┴───────────┴──────────┴────────────┘    │
└─────────────────────────────────────────────────────┘

Packages

PackageImportDescription
ormgithub.com/hanzoai/ormCore Model[T], registration, hooks, cache, serialization
orm/dbgithub.com/hanzoai/orm/dbDatabase interfaces + SQLite driver
orm/valgithub.com/hanzoai/orm/valStruct field validation
orm/internal/jsongithub.com/hanzoai/orm/internal/jsonJSON encode/decode helpers
orm/internal/reflectgithub.com/hanzoai/orm/internal/reflectReflection utilities

Key Features

FeatureDescription
Generics ModelModel[T] mixin -- embed once, get Id, Kind, CRUD, Query
Auto-RegistrationRegister[T]("kind") in init() -- no code generation
Auto-Serializationorm:"serialize" tag auto-marshals complex fields to JSON strings
Struct Tag Defaultsorm:"default:value" sets field defaults on creation
Lifecycle HooksBeforeCreate, AfterCreate, BeforeUpdate, AfterUpdate interfaces
Typed QueriesTypedQuery[T] returns []*T -- no type assertions
KV CacheRead-through caching with Memory LRU, Redis/Valkey, or no-op backends
SQLite DriverWAL mode, JSON storage, json_extract queries, sqlite-vec ready
ZAP DriverBinary protocol to PostgreSQL, MongoDB, Redis, ClickHouse via zap-sidecar
Namespace SupportMulti-tenant isolation with SetNamespace
Context PropagationCreateCtx, UpdateCtx, DeleteCtx for deadline/tracing support
Old-Entity HooksBeforeUpdate(old *T) receives the previous state for diff-aware logic
Must VariantsMustGet, MustNew -- panic on error for scripts and tests
GetOrCreate / GetOrUpdateAtomic find-or-initialize and find-or-modify helpers
Query.First / GetAll / CountSingle-result, context-aware list, and count queries

New in v0.3.0

Namespace Support

Namespace scoping enables multi-tenant isolation without separate tables or databases. All CRUD and query operations are automatically scoped to the active namespace.

user := orm.New[User](db)
user.SetNamespace("org-123") // all ops scoped to org-123

user.Name = "Alice"
user.Create() // stored under namespace "org-123"

// Queries are scoped too
q := orm.TypedQuery[User](db)
q.SetNamespace("org-123")
users, _ := q.Filter("Status=", "active").Get()

Context Propagation

All mutating operations now accept a context.Context for deadline enforcement, cancellation, and tracing propagation.

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

user := orm.New[User](db)
user.Name = "Alice"

if err := user.CreateCtx(ctx); err != nil {
    log.Fatal(err)
}

user.Name = "Alice Smith"
if err := user.UpdateCtx(ctx); err != nil {
    log.Fatal(err)
}

if err := user.DeleteCtx(ctx); err != nil {
    log.Fatal(err)
}

The original Create(), Update(), and Delete() methods still work and use context.Background() internally.

Old-Entity Hooks

BeforeUpdate and AfterUpdate hooks now receive the previous entity state, enabling diff-aware logic like audit logging, conditional side effects, and change detection.

func (u *User) BeforeUpdate(old *User) error {
    if old.Email != u.Email {
        // email changed -- send verification
        return sendVerificationEmail(u.Email)
    }
    return nil
}

func (u *User) AfterUpdate(old *User) error {
    if old.Status != u.Status {
        // status changed -- emit event
        emitStatusChange(u.Id(), old.Status, u.Status)
    }
    return nil
}

Must Variants and Helpers

Must* functions panic on error -- useful for tests, scripts, and initialization code where failure is not recoverable.

// Panics if user not found
user := orm.MustGet[User](db, "user-123")

// GetOrCreate: returns existing entity or creates a new one
entity, created, err := orm.GetOrCreate[User](db, "user-123", func(u *User) {
    u.Name = "Default"
    u.Status = "pending"
})
// created == true if a new entity was initialized

// GetOrUpdate: fetches then applies a mutation
entity, err := orm.GetOrUpdate[User](db, "user-123", func(u *User) {
    u.LastLogin = time.Now()
})

CloneFromJSON deserializes JSON into a new entity copy, and Zero returns a zero-valued entity of the registered kind -- both useful for testing and data migration.

clone, err := orm.CloneFromJSON[User](db, jsonBytes)
empty := orm.Zero[User](db)

Query Improvements

TypedQuery[T] gains three convenience methods:

q := orm.TypedQuery[User](db)

// First: returns the first match or ErrNotFound
user, err := q.Filter("Email=", "alice@example.com").First()
if errors.Is(err, orm.ErrNotFound) {
    // no match
}

// GetAll: context-aware list query
ctx := context.Background()
users, err := q.Filter("Status=", "active").GetAll(ctx)

// Count: returns the number of matching entities
count, err := q.Filter("Status=", "active").Count(ctx)

Serverless PostgreSQL for relational data and embeddings

Redis/Valkey for caching, sessions, and pub/sub

Billing platform built on Hanzo ORM

Go client for the Hanzo LLM Gateway

How is this guide?

Last updated on

On this page