Recipes
Copy-paste solutions to the tasks that come up most. Each links to the guide that explains it in depth. Assume ctx and an opened db are in scope.
Paginate
Section titled “Paginate”Keyset (“cursor”) pagination — stable and offset-free, the right choice for APIs:
id := query.Col[int64]("id")page, _ := query.Select[User](db). Filter(id.Gt(cursor)). // cursor = last id seen (0 for the first page) Order(query.Asc(id)). Limit(pageSize).All(ctx)// next cursor = page[len(page)-1].IDClassic offset pagination (portable to MSSQL’s OFFSET…FETCH):
page, _ := query.Select[User](db).Order(query.Asc(id)).Limit(pageSize).Offset(n).All(ctx)Insert many rows
Section titled “Insert many rows”users := []*User{{Name: "a"}, {Name: "b"}, {Name: "c"}}_ = orm.NewRepo[User](db).CreateInBatches(ctx, users, 500) // one multi-row INSERT per chunk// query side: query.NewRepo[User](db).InsertMany(ctx, users) (uses pgx CopyFrom when available)Upsert (insert or update on conflict)
Section titled “Upsert (insert or update on conflict)”_ = orm.NewRepo[Language](db).Upsert(ctx, &Language{Code: "en", Name: "English"}, query.OnConflict("code").DoUpdate("name")) // narrow DoUpdate cols to preserve e.g. created_atFetch many rows by primary key
Section titled “Fetch many rows by primary key”users, _ := orm.NewRepo[User](db).GetByKeys(ctx, 1, 2, 3) // one WHERE id IN (...) queryProcess a large table without loading it all
Section titled “Process a large table without loading it all”Stream row-by-row with Iter (a Go 1.23 range-over-func):
for u, err := range query.Select[User](db).Where("active = ?", true).Iter(ctx) { if err != nil { return err } // handle u}Or process in fixed-size chunks (keyset under the hood):
err := orm.NewRepo[User](db).Where("active = ?", true). FindInBatches(ctx, 1000, func(batch []User) error { // handle batch return nil })Atomically increment a column
Section titled “Atomically increment a column”Compute the new value in SQL — no read-modify-write race:
_, err := query.Update[Post](db).SetExpr("views", "views + ?", 1).Where("id = ?", id).Exec(ctx)Eager-load only some related rows
Section titled “Eager-load only some related rows”Filter and order the batched children query (still one query, N+1-safe):
orm.Load[Author, Post](ctx, db, authors, "Posts", orm.LoadWhere("published_at IS NOT NULL"), orm.LoadOrderBy("published_at DESC"))Pull a single column into a slice
Section titled “Pull a single column into a slice”emails, _ := query.Pluck(ctx, query.Select[User](db).Filter(active), query.Col[string]("email")) // []stringSoft delete and restore
Section titled “Soft delete and restore”posts := orm.NewRepo[Post](db)posts.Delete(ctx, &p) // soft delete (sets deleted_at); excluded from readsposts.Restore(ctx, &p) // un-delete: deleted_at back to NULLposts.ForceDelete(ctx, &p) // hard delete, even on a soft-delete modelgone, _ := posts.OnlyDeleted().Find(ctx) // the deleted rowsRetry a transaction on a transient failure
Section titled “Retry a transaction on a transient failure”IsRetryable is true for a deadlock or serialization failure — roll back and try again:
for attempt := 0; attempt < 3; attempt++ { tx, err := db.Begin(ctx) if err != nil { return err } err = doWork(ctx, tx) // your repo/query calls on tx if err == nil { if err = tx.Commit(ctx); err == nil { return nil } } _ = tx.Rollback(ctx) if !liteorm.IsRetryable(err) { return err }}return errors.New("gave up after retries")Map errors at an HTTP boundary
Section titled “Map errors at an HTTP boundary”switch {case liteorm.IsNotFound(err): return c.JSON(404, ...)case liteorm.IsUniqueViolation(err): return c.JSON(409, ...) // duplicatecase liteorm.IsForeignKeyViolation(err): return c.JSON(409, ...)case err != nil: return c.JSON(500, ...)}See also
Section titled “See also”- Cheat sheet — the whole surface at a glance.
- Deep guides: query · ORM · associations · soft delete · errors · transactions.