Collections

Table of contents
  1. Collections
    1. Introduction
    2. Available methods
      1. Count, IsEmpty, First, Last
      2. Each, Filter, Reject
      3. MapTo and Reduce
      4. Pluck and PluckAs
      5. ModelKeys, Find, Contains
      6. Only and Except
      7. KeyBy, GroupBy, Unique
    3. Loading relations
    4. Fresh
    5. Serialization
    6. Collections and relationships
    7. Choosing between a collection and a query
    8. Testing

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 (uint for ID, string for Slug), so index with uint(1), not 1.
  • Pointer columns (CategoryID *uint) key by the dereferenced value, and NULL rows are skipped by KeyBy and GroupBy.
  • KeyBy keeps the last model for a repeated key; the map holds plain copies. GroupBy buckets are bound collections like Filter results.
  • Unique keeps the first model for each distinct value (NULL counts 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.