DEV Community

Reza Khademi
Reza Khademi

Posted on

ZORM: A Zero-Tag, Generic ORM for Golang

ZORM: A Zero-Tag, Generic ORM for Golang
ZORM is a modern ORM for Go that lets you write fluent, type-safe, SQL-like queries using Go generics — all without struct tags or heavy reflection.

It is built for developers who want:

  • the ergonomics of an ORM,
  • the clarity of handwritten SQL,
  • and the safety guarantees of Go’s type system.

👉 GitHub: https://github.com/rezakhademix/zorm


The Problem with Typical Go ORMs

Many Go ORMs rely on:

  • struct tags for mapping
  • string-based queries with limited compile-time guarantees

This often leads to:

  • duplicated schema definitions
  • silent runtime errors due to typos
  • harder refactoring when models change

ZORM takes a different path.


What Makes ZORM Different?

1. No Struct Tags

You define a plain Go struct:

type User struct {
    ID        int64
    Name      string
    Email     string
    CreatedAt time.Time
}
Enter fullscreen mode Exit fullscreen mode

ZORM automatically infers:

table name: users

columns: id, name, email, created_at
Enter fullscreen mode Exit fullscreen mode

No db:"..." or json:"..." style tags are required for persistence.

This reduces boilerplate and keeps your models clean and idiomatic.

2. ZORM Type Safety with Generics

ZORM is built on Go generics, so queries are bound to a concrete type at compile time.

users, err := zorm.New[User]().Get(ctx)
Enter fullscreen mode Exit fullscreen mode

3. Fluent, SQL-Like Query Builder

Queries are chainable and readable:

admins, err := zorm.New[User]().
    Where("role =", "admin").
    OrderBy("created_at DESC").
    Limit(10).
    Get(ctx)
Enter fullscreen mode Exit fullscreen mode

You get composable queries without writing raw SQL everywhere.

You can also inspect the generated SQL:

q := zorm.New[User]().Where("age >", 18)
fmt.Println(q.Print())

Enter fullscreen mode Exit fullscreen mode

Quick Start

  • Install

go get github.com/rezakhademix/zorm

Enter fullscreen mode Exit fullscreen mode
  • Connect to the database
db, err := sql.Open("postgres", dsn)
if err != nil {
    log.Fatal(err)
}
defer db.Close()


zorm.SetDB(db)
Enter fullscreen mode Exit fullscreen mode
  • ZORM Insert query
u := User{
    Name:  "Alice",
    Email: "alice@example.com",
}


err = zorm.New[User]().Insert(ctx, &u)
Query records
users, err := zorm.New[User]().
    Where("email LIKE", "%@example.com").
    OrderBy("id DESC").
    Get(ctx)
Enter fullscreen mode Exit fullscreen mode

ZORM supports common relationships such as:

  • HasOne
  • HasMany
  • BelongsTo
  • Polymorphic relations
type User struct {
    ID      int64
    Name    string
    Posts   []*Post  // HasMany
    Profile *Profile // HasOne
}

// HasMany: User has many Posts
// Method can be named "Posts" or "PostsRelation"
func (u User) PostsRelation() zorm.HasMany[Post] {
    return zorm.HasMany[Post]{
        ForeignKey: "user_id",  // Column in posts table
        LocalKey:   "id",       // Optional, defaults to primary key
    }
}

// HasOne: User has one Profile
func (u User) ProfileRelation() zorm.HasOne[Profile] {
    return zorm.HasOne[Profile]{
        ForeignKey: "user_id",
    }
}

type Post struct {
    ID     int64
    UserID int64
    Title  string
    Author *User    // BelongsTo
}

// BelongsTo: Post belongs to User
func (p Post) AuthorRelation() zorm.BelongsTo[User] {
    return zorm.BelongsTo[User]{
        ForeignKey: "user_id",  // Column in posts table
        OwnerKey:   "id",       // Optional, defaults to primary key
    }
}

Enter fullscreen mode Exit fullscreen mode

You can eager load related data without manual joins, keeping query logic centralized and reusable.

  • Transactions

Transactions are explicit and composable:

err := zorm.Transaction(ctx, func(tx *zorm.Tx) error {
    if err := tx.New[User]().Insert(ctx, &u); err != nil {
        return err
    }


    if err := tx.New[Profile]().Insert(ctx, &p); err != nil {
        return err
    }


    return nil
})
Enter fullscreen mode Exit fullscreen mode

If any step fails, everything is rolled back.

Advanced Capabilities

ZORM is not limited to simple CRUD:

  • prepared statement caching
  • context-aware execution
  • subqueries and nested queries
  • support for complex SQL constructs when needed
  • Sync - Synchronize Associations
// Current roles in DB: [1, 2, 3]

// Sync to new set of roles
err := zorm.New[User]().Sync(ctx, user, "Roles", []any{1, 2, 4}, nil)
// Result:
// - Role 1: kept (exists in both)
// - Role 2: kept (exists in both)
// - Role 3: detached (was in DB, not in new list)
// - Role 4: attached (not in DB, is in new list)
// Final roles in DB: [1, 2, 4]
Enter fullscreen mode Exit fullscreen mode

Main/Replica Splitting

zorm.ConfigureDBResolver(
    zorm.WithPrimary(primaryDB),
    zorm.WithReplicas(replica1, replica2),
    zorm.WithLoadBalancer(zorm.RoundRobinLB),
)

// Automatic routing
users, _ := zorm.New[User]().Get(ctx)          // Reads from replica
err := zorm.New[User]().Create(ctx, user)      // Writes to primary

// Force primary for consistency
users, _ := zorm.New[User]().UsePrimary().Get(ctx)

// Force specific replica
users, _ := zorm.New[User]().UseReplica(0).Get(ctx)
Enter fullscreen mode Exit fullscreen mode

Common Table Expressions (CTEs)


// String CTE
users, _ := zorm.New[User]().
    WithCTE("active_users", "SELECT * FROM users WHERE active = true").
    Raw("SELECT * FROM active_users WHERE age > 18").
    Get(ctx)

// Subquery CTE
subQuery := zorm.New[User]().Where("active", true)
users, _ := zorm.New[User]().
    WithCTE("active_users", subQuery).
    Raw("SELECT * FROM active_users").
    Get(ctx)

Enter fullscreen mode Exit fullscreen mode

You can stay high-level for most operations and drop lower when necessary.

FAQ

1. Does ZORM use reflection?

ZORM minimizes runtime reflection and relies primarily on Go generics and compile-time type information. This reduces overhead and avoids many of the runtime surprises common in reflection-heavy ORMs.


2. Why are there no struct tags?

ZORM follows convention over configuration.

Field names are automatically mapped from CamelCase to snake_case, and table names are inferred from the struct name. This removes duplication between struct definitions and tag metadata and makes refactoring safer and simpler.


3. Is ZORM faster than other Go ORMs?

ZORM is designed to be lightweight and predictable by avoiding heavy reflection and unnecessary abstractions. Actual performance depends on your queries, database, and indexes, but the goal is to stay close to the cost of using database/sql directly while providing higher-level ergonomics.


4. Can I run raw SQL queries?

Yes.

ZORM focuses on query building and mapping, not restricting SQL. You can always fall back to raw SQL when needed and still scan results into your structs.


5. Does ZORM support transactions?

Yes.

Transactions are explicit and type-safe. You pass a function that receives a transaction handle, and ZORM commits or rolls back automatically based on the returned error.


6. Which databases are supported?

Any database that works with Go’s standard database/sql drivers (e.g. PostgreSQL, MySQL, SQLite, SQL Server).

ZORM is driver-agnostic and does not lock you into a specific backend.


7. Does ZORM handle migrations?

No.

ZORM is focused on querying and data access. Schema migrations should be handled by dedicated tools such as Goose, Golang-Migrate depending on your stack.


8. Can I define relationships between models?

Yes.

Common relations like one-to-one, one-to-many, and polymorphic associations are supported, with optional eager loading to reduce manual join boilerplate.


9. Is ZORM safe to use concurrently?

Yes.

Query builders are independent values. As long as your underlying *sql.DB is safe for concurrent use (which it is by design), ZORM operations can be used across goroutines.


10. When should I not use ZORM?

If you require:

  • automatic schema generation and migrations
  • database-first code generation
  • deep integration with a specific database vendor feature set

In those cases, a migration framework or SQL code generator may be a better primary tool. ZORM is optimized for clean, type-safe query construction and data mapping in idiomatic Go.

Top comments (0)