Collections
Table of contents
Introduction
Every query that returns many models returns an orm.Collection[T]: Get, All, FindMany, Paginate (Page.Data) and a many relation’s Get/Items. It is Eloquent’s Collection, with helpers for the things you do with a list of models.
users, err := orm.New[User](db).Query().Where("active = ?", true).Get(ctx) // orm.Collection[User]
fmt.Println(users.Count(), users.First().Name)
A Collection[T] is a defined slice type (type Collection[T any] []T), so it is also a plain slice:
for i := range users { fmt.Println(users[i].Email) } // index, range, len, append
var list []User = users // assignable both ways, no conversion
users = append(users, extra)
Existing code that expected []User keeps compiling.
Go copies structs by value. Iterate with an index (&users[i]) or use the callbacks below, which hand you pointers, when you need to modify a model or call Save or a lazy relation on it. for _, u := range users gives you a detached copy.
Available methods
Methods that need a column or the primary key (Pluck, KeyBy, Find, …) require bound models and return UNBOUND_MODEL on a collection of hand-built structs. An unknown column is INVALID_SCHEMA.
| Method | Returns |
|---|---|
Count, IsEmpty, First, Last |
size and ends |
Each |
walks with an index and a pointer |
Filter, Reject |
a smaller collection |
MapTo, Reduce |
another type / one value |
Pluck, PluckAs |
one column’s values |
ModelKeys |
the primary keys |
Find, Contains |
lookup by primary key |
Only, Except |
subset by primary key |
KeyBy, GroupBy, Unique |
index / buckets / distinct |
Load, LoadMissing |
eager-load relations |
Fresh |
reload from the database |
ToMaps, JSON |
serialize |
Count, IsEmpty, First, Last
users.Count() // 3
users.IsEmpty() // false
users.First() // *User, or nil when empty
users.Last() // *User, or nil
First and Last return a pointer into the collection, so editing it edits the element.
Each, Filter, Reject
err := users.Each(func(i int, u *User) error {
return u.Update(ctx, map[string]any{"synced": true}) // return an error to stop the walk
})
admins := users.Filter(func(u *User) bool { return u.Role == "admin" })
others := users.Reject(func(u *User) bool { return u.Role == "admin" })
Filter and Reject return a new collection. Its models are copies that stay bound to the database, so Save, Refresh and lazy relations work on them, and they keep their dirty state. Editing a copy does not change the original collection.
MapTo and Reduce
Go methods cannot add type parameters, so these two are functions:
emails := orm.MapTo(users, func(u *User) string { return u.Email }) // []string
total := orm.Reduce(orders, 0.0, func(sum float64, o *Order) float64 { return sum + o.Total })
Pluck and PluckAs
ids, err := users.Pluck("id") // []any
names, err := orm.PluckAs[User, string](users, "name") // []string
Pluck takes a column name (id, created_at). PluckAs asserts each value to the type you ask for; a mismatch is CAST_FAILED rather than a panic. To pluck in SQL instead of in memory, use orm.Pluck(ctx, query, col).
ModelKeys, Find, Contains
keys, err := users.ModelKeys() // the primary keys, in order
u, err := users.Find(2) // *User, or nil — no error when absent
has, err := users.Contains("2") // true
Keys compare by printed value, so 2, uint(2) and "2" all match. Find searches the collection in memory; it is not Model.Find.
Only and Except
some, err := users.Only(1, 3) // the models with those keys
rest, err := users.Except(1, 3) // everything else
KeyBy, GroupBy, Unique
byID, err := users.KeyBy("id") // map[any]User
byID[uint(1)].Name // keys keep the field's Go type
byAuthor, err := posts.GroupBy("user_id") // map[any]orm.Collection[Post]
byAuthor[uint(1)].Count()
firstPerAuthor, err := posts.Unique("user_id") // the first post of each author
- Map keys keep the Go type of the field (
uintforID,stringforSlug), so index withuint(1), not1. - Pointer columns (
CategoryID *uint) key by the dereferenced value, andNULLrows are skipped byKeyByandGroupBy. KeyBykeeps the last model for a repeated key; the map holds plain copies.GroupBybuckets are bound collections likeFilterresults.Uniquekeeps the first model for each distinct value (NULLcounts as one value).
Loading relations
Load eager-loads relations onto every model with one query per relation, the same engine as With:
users, _ := orm.New[User](db).Query().Get(ctx) // decided later that we need posts
err := users.Load(ctx, "Posts.Comments", "Roles") // 3 queries total, not 3 per user
LoadMissing loads a relation only onto the models that do not have it yet, and never replaces one already loaded:
err := users.LoadMissing(ctx, "Posts")
Both take the same dotted paths as With. On an empty collection they do nothing. Use them when the decision to load comes after the query (for example, a branch in a service), and With when you know up front.
Fresh
Fresh reloads the same rows from the database and returns new models, in the original order; rows deleted in the meantime are dropped:
latest, err := users.Fresh(ctx)
The originals are untouched (compare with Refresh, which updates one model in place). Soft-deleted rows and rows hidden by a global scope count as gone.
Serialization
maps, err := users.ToMaps() // []map[string]any
js, err := users.JSON() // a JSON array
js, err = users.JSON(orm.MakeHidden("email"), orm.DateFormat("2006-01-02"))
Same options as serialization. For API shapes use a resource: UserResource.Collection(users).
Collections and relationships
A many relation hands out a collection too, so the helpers work on related models:
posts, _ := user.Posts.Get(ctx) // orm.Collection[Post]
published := user.Posts.Items().Filter(func(p *Post) bool { return p.Published })
Items() reads what is loaded without a query; Get(ctx) loads on first use.
Choosing between a collection and a query
Filtering in memory is right for a few hundred rows you already have. For anything larger, or anything the database does well, put the condition in the query (scopes help) and keep the collection for what SQL cannot do:
| Prefer | When |
|---|---|
Where / a scope |
the set is large, or you only need part of it |
Filter |
the rows are already loaded and the test is Go code |
orm.Pluck (query) |
you need one column of a large table |
Collection.Pluck |
you already hold the models |
Chunk |
walking a big table without holding it all |
Testing
integration/collections_test.go covers return types across Get, All, FindMany, Paginate and relations, Filter/Reject keeping models bound and saveable, every column helper, Load versus LoadMissing (identity of already-loaded rows), and Fresh keeping order while dropping deleted rows.