An Ent Cheat Sheet for Schemas Queries and Mutations
An Ent cheat sheet for Go and PostgreSQL covering schema options, create, query, edges, update, aggregate, transactions and errors, each checked by a run

This is a reference for the Ent calls you use every week, in tables with the program that exercised them. Every call was run against PostgreSQL 18.6 and the output is shown. The model is the library from the Ent tutorial: authors with books, a unique email, validated titles and pages. For the explanations behind each row, see schema, queries and transactions and hooks. For the same sheet in GORM, see the GORM cheat sheet.
Tested with Go 1.25.6, Ent v0.14.5, pgx v5.11.0, PostgreSQL 18.6 in Docker, macOS on Apple Silicon (arm64)
The program
package main
import (
"context"
"errors"
"fmt"
"example.test/lab/ent"
"example.test/lab/ent/author"
"example.test/lab/ent/book"
)
func main() {
ctx := context.Background()
client := open("l0755")
defer client.Close()
must(client.Schema.Create(ctx))
// create
ada := client.Author.Create().SetName("Ada").SetEmail("[email protected]").SaveX(ctx)
client.Book.CreateBulk(
client.Book.Create().SetTitle("A").SetPages(120).SetAuthor(ada).SetPublished(true),
client.Book.Create().SetTitle("B").SetPages(300).SetAuthor(ada),
client.Book.Create().SetTitle("C").SetPages(450).SetAuthor(ada).SetPublished(true),
).ExecX(ctx)
fmt.Println("create:", ada.ID, client.Book.Query().CountX(ctx), "books")
// read
first := client.Book.Query().Order(ent.Asc(book.FieldTitle)).FirstX(ctx)
only := client.Book.Query().Where(book.Title("B")).OnlyX(ctx)
byID := client.Book.GetX(ctx, only.ID)
pages := client.Book.Query().Where(book.PagesGTE(300)).Order(ent.Desc(book.FieldPages)).AllX(ctx)
titles := client.Book.Query().Where(book.Published(true)).Order(ent.Asc(book.FieldTitle)).Select(book.FieldTitle).StringsX(ctx)
fmt.Println("read:", first.Title, only.Title, byID.Pages, len(pages), titles)
fmt.Println("exist/count/ids:", client.Book.Query().Where(book.PagesGT(400)).ExistX(ctx), client.Book.Query().Where(book.Published(false)).CountX(ctx), client.Book.Query().Order(ent.Asc(book.FieldID)).IDsX(ctx))
// predicates
n := client.Book.Query().Where(book.Or(book.PagesLT(150), book.And(book.Published(true), book.Not(book.TitleEQ("C"))))).CountX(ctx)
fmt.Println("or/and/not:", n)
// edges
withBooks := client.Author.Query().Where(author.HasBooksWith(book.PagesGT(400))).WithBooks().OnlyX(ctx)
fmt.Println("edges:", withBooks.Name, len(withBooks.Edges.Books), "books;", ada.QueryBooks().CountX(ctx), "via QueryBooks;", only.QueryAuthor().OnlyX(ctx).Name, "via QueryAuthor")
// update
b2 := only.Update().SetPublished(true).AddPages(5).SaveX(ctx)
rows := client.Book.Update().Where(book.PagesLT(200)).SetPublished(false).SaveX(ctx)
fmt.Println("update:", b2.Pages, b2.Published, "| bulk rows:", rows)
// aggregate
var agg []struct {
Sum int `json:"sum"`
Max int `json:"max"`
}
client.Book.Query().Aggregate(ent.Sum(book.FieldPages), ent.Max(book.FieldPages)).ScanX(ctx, &agg)
fmt.Println("aggregate:", agg)
// transaction
tx, _ := client.Tx(ctx)
tx.Book.Create().SetTitle("D").SetPages(10).SetAuthorID(ada.ID).SaveX(ctx)
tx.Rollback()
fmt.Println("rolled back, books:", client.Book.Query().CountX(ctx))
// errors
_, err := client.Book.Get(ctx, 999)
fmt.Println("errors:", ent.IsNotFound(err))
_, err = client.Author.Create().SetName("Dup").SetEmail("[email protected]").Save(ctx)
fmt.Println(" constraint:", ent.IsConstraintError(err))
_, err = client.Book.Create().SetTitle("").SetPages(1).SetAuthor(ada).Save(ctx)
var ve *ent.ValidationError
fmt.Println(" validation:", ent.IsValidationError(err), errors.As(err, &ve))
_, err = client.Book.Query().Where(book.PagesGT(1)).Only(ctx)
fmt.Println(" not singular:", ent.IsNotSingular(err))
// delete
deleted := client.Book.Delete().Where(book.Published(false)).ExecX(ctx)
client.Book.DeleteOneID(only.ID).ExecX(ctx)
fmt.Println("delete:", deleted, "left", client.Book.Query().CountX(ctx))
}package main
import (
"database/sql"
"log"
"entgo.io/ent/dialect"
entsql "entgo.io/ent/dialect/sql"
"example.test/lab/ent"
_ "github.com/jackc/pgx/v5/stdlib"
)
const base = "postgres://postgres:[email protected]:55432/lab?sslmode=disable"
// open recreates a schema and returns a client whose connections use it.
func open(schema string, opts ...ent.Option) *ent.Client {
admin, err := sql.Open("pgx", base)
if err != nil {
log.Fatal(err)
}
admin.Exec("drop schema if exists " + schema + " cascade")
admin.Exec("create schema " + schema)
admin.Close()
db, err := sql.Open("pgx", base+"&search_path="+schema)
if err != nil {
log.Fatal(err)
}
drv := entsql.OpenDB(dialect.Postgres, db)
return ent.NewClient(append([]ent.Option{ent.Driver(drv)}, opts...)...)
}
func must(err error) {
if err != nil {
log.SetFlags(0)
log.Fatal(err)
}
}package ent
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schemapackage schema
import (
"time"
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
)
type Author struct{ ent.Schema }
func (Author) Fields() []ent.Field {
return []ent.Field{
field.String("name").NotEmpty(),
field.String("email").Unique(),
field.Time("created_at").Default(time.Now).Immutable(),
}
}
func (Author) Edges() []ent.Edge { return []ent.Edge{edge.To("books", Book.Type)} }package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
)
type Book struct{ ent.Schema }
func (Book) Fields() []ent.Field {
return []ent.Field{
field.String("title").NotEmpty(),
field.Int("pages").Positive(),
field.Bool("published").Default(false),
}
}
func (Book) Edges() []ent.Edge {
return []ent.Edge{edge.From("author", Author.Type).Ref("books").Unique().Required()}
}
func (Book) Indexes() []ent.Index { return []ent.Index{index.Fields("title").Edges("author").Unique()} }set -e
GOFLAGS=-mod=mod go generate ./ent
go mod tidy > /dev/null 2>&1
go run .create: 1 3 books
read: A B 300 2 [A C]
exist/count/ids: true 1 [1 2 3]
or/and/not: 1
edges: Ada 3 books; 3 via QueryBooks; Ada via QueryAuthor
update: 305 true | bulk rows: 1
aggregate: [{875 450}]
rolled back, books: 3
errors: true
constraint: true
validation: true true
not singular: true
delete: 1 left 1Setup and generation
| Need | Command or call |
|---|---|
| Generate the client | go generate ./ent with //go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema |
| Enable a feature | generate --feature sql/upsert,privacy,intercept,entql ./schema |
| Open a client | ent.NewClient(ent.Driver(entsql.OpenDB(dialect.Postgres, sqlDB))) |
| Create tables (development) | client.Schema.Create(ctx) |
| Preview the migration | client.Schema.WriteTo(ctx, os.Stdout) |
| Log every statement | ent.Debug(), or ent.Log(fn) for your own logger |
Schema
| Need | Option |
|---|---|
| Required string | field.String("name").NotEmpty() |
| Unique | .Unique() |
| Optional, nullable column | .Optional() |
| Pointer in Go for NULL | .Optional().Nillable() |
| Default | .Default(v) or .Default(time.Now) |
| Update default | .UpdateDefault(time.Now) |
| Set once | .Immutable() |
| Range | .Min(0), .Positive(), .Range(1, 5) |
| Length | .MaxLen(30) |
| Custom check | .Validate(func(v string) error { ... }) |
| Enum | field.Enum("plan").Values("free", "pro").Default("free") |
| JSON | field.JSON("links", []string{}) |
| One to many | edge.To("books", Book.Type) and edge.From("author", Author.Type).Ref("books").Unique().Required() |
| Named foreign key | .Field("author_id") on the edge, with field.Int("author_id") |
| Many to many with data | .Through("enrollments", Enrollment.Type) (guide) |
| Composite index | index.Fields("title").Edges("author").Unique() |
| Table name | entsql.Annotation{Table: "user_profiles"} |
| Shared fields | a Mixin() (mixins) |
Create
| Need | Call |
|---|---|
| One row | client.Author.Create().SetName("Ada").SetEmail("[email protected]").Save(ctx) |
| Panic on error (tests) | SaveX(ctx) |
| Set an edge | SetAuthor(a) or SetAuthorID(id) |
| Many rows | client.Book.CreateBulk(b1, b2, b3).Save(ctx) |
| Optional pointer | SetNillablePriority(p) |
Upsert (feature sql/upsert) | .OnConflictColumns(account.FieldEmail).UpdateNewValues().Exec(ctx) |
The program's first line, create: 1 3 books, is the author id and the number of books after a bulk create.
Query
| Need | Call |
|---|---|
| By id | client.Book.Get(ctx, id) |
| First, or NotFound | Query().Order(ent.Asc(book.FieldTitle)).First(ctx) |
| Exactly one | Query().Where(book.Title("B")).Only(ctx) |
| Many | Query().Where(book.PagesGTE(300)).Order(ent.Desc(book.FieldPages)).All(ctx) |
| One column | Select(book.FieldTitle).Strings(ctx) |
| Exists, count, ids | Exist(ctx), Count(ctx), IDs(ctx) |
| Paging | Limit(20).Offset(40) |
| Compose | book.Or(a, b), book.And(a, b), book.Not(a) |
The output read: A B 300 2 [A C] is the first title, the only title, its pages, two long books and the published titles. exist/count/ids: true 1 [1 2 3] is the exist check, the unpublished count and the id list. or/and/not: 1 is the composed predicate.
Edges
| Need | Call |
|---|---|
| Filter by a related row | author.HasBooksWith(book.PagesGT(400)) |
| Has any | author.HasBooks() |
| Eager load | Query().WithBooks().All(ctx) |
| Eager load with options | WithBooks(func(q *ent.BookQuery) { q.Order(...).Where(...) }) |
| Traverse from an entity | ada.QueryBooks().All(ctx), book.QueryAuthor().Only(ctx) |
edges: Ada 3 books; 3 via QueryBooks; Ada via QueryAuthor shows eager loading and both traversals. A Limit inside WithBooks limits the whole child query, not each parent (queries).
Update and aggregate
| Need | Call |
|---|---|
| Update one | b.Update().SetPublished(true).Save(ctx) or UpdateOneID(id) |
| Arithmetic in SQL | AddPages(5) |
| Clear an optional field | ClearBio() |
| Bulk | client.Book.Update().Where(book.PagesLT(200)).SetPublished(false).Save(ctx) returns rows changed |
| Aggregates | Query().Aggregate(ent.Sum(book.FieldPages), ent.Max(book.FieldPages)).Scan(ctx, &rows) |
| Group by | GroupBy(book.AuthorColumn).Aggregate(ent.Count()).Scan(ctx, &rows) |
update: 305 true | bulk rows: 1 and aggregate: [{875 450}] are the arithmetic update, the bulk row count, and the sum and max of pages. Scan more than one aggregate into a struct; Ints reads a single column.
Transactions
| Need | Call |
|---|---|
| Begin | tx, err := client.Tx(ctx) |
| Use | tx.Book.Create()..., never the outer client |
| End | tx.Commit() or tx.Rollback() |
rolled back, books: 3 shows a book created in a transaction disappearing on rollback. Wrap it in a helper that rolls back on error and panic (the helper).
Errors and delete
| Test | Meaning |
|---|---|
ent.IsNotFound(err) | Get, First or Only found nothing |
ent.IsNotSingular(err) | Only found more than one |
ent.IsConstraintError(err) | unique, foreign key or similar |
ent.IsValidationError(err) | a schema validator failed, before any SQL |
errors.As(err, &ve) with *ent.ValidationError | read the failing field |
| Delete | Call |
|---|---|
| One | client.Book.DeleteOneID(id).Exec(ctx) |
| By condition | client.Book.Delete().Where(book.Published(false)).Exec(ctx) returns rows removed |
The four error tests all printed true, and delete: 1 left 1 is the bulk delete count and the remaining books.
Five habits
- Pass
ctxto every terminal call. - Use typed predicates, never strings.
- Eager load relations you are about to read for many parents.
- Print the SQL when a query is new (
ent.Debug()). - Map
IsNotFound,IsConstraintErrorandIsValidationErrorto domain errors in one place.
The full reference is the Ent documentation, and the predicates and builders are listed in the CRUD API guide.


