Skip to content

CRUD with the Repo

orm.NewRepo[T] gives you a typed repository over a model: the read and write verbs for one table, with lifecycle hooks, auto timestamps, and the soft-delete scope handled for you. This page is the tour of those verbs — for the model structs themselves see declaring models, and for reusable read filters see scopes.

For exhaustive API detail, see the reference at pkg.go.dev/liteorm.org/orm.

orm.NewRepo[T](sess) takes a session — a *liteorm.DB or a transaction — and returns a *orm.Repo[T]. Building one is cheap and does no I/O, so make one per request or reuse a package-level value; either way it runs against whatever session you pass in.

posts := orm.NewRepo[Post](db)
// Create: fires Before/AfterCreate hooks, stamps auto timestamps, reads the
// generated primary key back into v.
p := Post{AuthorID: ada.ID, Title: "Generics in Go"}
err := posts.Create(ctx, &p)
fmt.Println(p.ID, p.CreatedAt)
// Get by primary key (→ liteorm.ErrNoRows when absent or soft-deleted). For a
// composite key, pass one value per column in declaration order: Get(ctx, a, b).
got, err := posts.Get(ctx, p.ID)
// GetByKeys: fetch many rows by a list of primary keys in one query (single-PK).
some, err := posts.GetByKeys(ctx, id1, id2, id3)
// Find: all rows in the current scope (soft-deleted rows excluded by default).
all, err := posts.Find(ctx)
// Update non-key columns; fires Before/AfterUpdate, bumps autoupdatetime.
p.Views = 42
err = posts.Update(ctx, &p)
// Delete: a soft delete when the model has a soft_delete column, else a hard
// delete. Always scoped by primary key. Fires Before/AfterDelete.
err = posts.Delete(ctx, &p)

A keyed Update or Delete that matches no row — a primary key that isn’t there, or a soft-deleted row that’s out of the current scope — returns liteorm.ErrNoRows rather than silently succeeding, so a no-op write is something you can detect (and the After hook does not fire). To reach a soft-deleted row on purpose, scope in with IncludeDeleted() first.

On top of the core verbs the Repo carries the ergonomic write helpers you reach for most, each composed from the hook-firing primitives above:

// Save: insert when the primary key is zero, update otherwise — upsert by identity.
err := posts.Save(ctx, &p)
// Upsert: INSERT ... ON CONFLICT DO UPDATE in one statement. Name the conflict
// column(s) and which columns to update (narrow them to preserve e.g. created_at).
// Use .DoNothing() instead to ignore a conflicting row (portable INSERT OR IGNORE).
err = posts.Upsert(ctx, &p, query.OnConflict("slug").DoUpdate("title", "body"))
// FirstOrCreate: load the first row matching the conditions, or insert v if none
// exists. created reports which path it took; the conditions are the lookup, v
// supplies the new row.
created, err := posts.FirstOrCreate(ctx, &p, query.Col[string]("slug").Eq("hello"))
// FirstOrInit: the non-persisting sibling — load the match, or leave v as the
// defaults you set and write nothing. found reports whether a row was loaded.
found, err := posts.FirstOrInit(ctx, &p, query.Col[string]("slug").Eq("hello"))

Updates writes only the columns you name (matched by column or Go field name); with no columns it is a full Update. Select and Omit return a scoped Repo view that narrows which columns any write touches, so one struct can drive a partial write without zeroing the columns you leave out:

// write only the named columns
err = posts.Updates(ctx, &p, "title", "body")
// only title and body are written; everything else on the row is left as-is
err = posts.Select("title", "body").Update(ctx, &p)
// write every writable column except internal_notes
err = posts.Omit("internal_notes").Update(ctx, &p)

The primary key and auto-timestamp columns are still managed for you under Select/Omit; the scope governs the ordinary data columns. The soft-delete scope still applies, so a keyed Save/Updates matching no in-scope row returns liteorm.ErrNoRows rather than silently resurrecting a deleted row.

Batches: CreateInBatches and FindInBatches

Section titled “Batches: CreateInBatches and FindInBatches”

For bulk writes, CreateInBatches inserts many rows in chunks of N — one multi-row INSERT per chunk — firing per-row hooks and reading generated keys back into each element:

err := posts.CreateInBatches(ctx, []*Post{&p1, &p2, &p3}, 100)

For a large result set you don’t want to hold in memory, FindInBatches walks the table in keyset-ordered chunks, calling your function once per batch. It honors the current scopes and the soft-delete filter, requires a single-column primary key (it imposes its own primary-key ordering, so don’t combine it with OrderBy), and stops when a batch is short or your function returns an error:

err := posts.Where("views > ?", 0).FindInBatches(ctx, 500, func(batch []Post) error {
for i := range batch {
// process batch[i] …
}
return nil
})

For row-at-a-time streaming instead, range over query.Select[Post](db).Iter(ctx) — an iter.Seq2[Post, error].

Find isn’t all-or-nothing. The Repo carries a thin read surface — Where, Filter, OrderBy, Limit, Offset — that composes onto the query builder, plus the finishers First, Count, and Exists. Each returns a Repo view, so you chain them and the soft-delete scope still applies underneath:

recent, _ := posts.Where("views > ?", 100).OrderBy("created_at DESC").Limit(10).Find(ctx)
top, _ := posts.OrderBy("views DESC").First(ctx)
n, _ := posts.Where("author_id = ?", ada.ID).Count(ctx)
any, _ := posts.Filter(query.Col[string]("slug").Eq("hello")).Exists(ctx)

Where takes a raw fragment with ? placeholders; Filter takes typed query.Col[V] predicates. When you need joins, unions, projections, or grouping, build a query.Select[T] on the same session directly — the Repo composes the query builder rather than forking it. For filters you reuse, package them as scopes.

  • Scopes — package and compose the filters above.
  • Soft deleteIncludeDeleted, OnlyDeleted, ForceDelete, Restore.
  • Hooks — the lifecycle callbacks Create/Update/Delete fire.
  • Associations — loading and writing relations.
  • Declaring models — the structs these verbs operate on.