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.
Constructing a Repo
Section titled “Constructing a Repo”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)The core verbs
Section titled “The core verbs”// 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 = 42err = 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.
Save, Upsert, and find-or-create
Section titled “Save, Upsert, and find-or-create”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"))Partial writes: Updates, Select, Omit
Section titled “Partial writes: Updates, Select, Omit”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 columnserr = posts.Updates(ctx, &p, "title", "body")
// only title and body are written; everything else on the row is left as-iserr = posts.Select("title", "body").Update(ctx, &p)
// write every writable column except internal_noteserr = 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].
Filtered reads
Section titled “Filtered reads”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.
Where to next
Section titled “Where to next”- Scopes — package and compose the filters above.
- Soft delete —
IncludeDeleted,OnlyDeleted,ForceDelete,Restore. - Hooks — the lifecycle callbacks Create/Update/Delete fire.
- Associations — loading and writing relations.
- Declaring models — the structs these verbs operate on.