
Your Ent app works CRUD, type-safe queries, migrations, edges all good. But real-world needs like fuzzy search, atomic counters, and row-level locking quickly go beyond the basics.
We’ve been running Ent across 100+ schemas on a Go backend that processes real payments. An AuditMixin ensures consistent audit logs, and custom decimal.Decimal mappings keep money calculations precise across orders and currencies.
What is Ent?
Ent is a ORM (from Meta) that uses code generation instead of reflection. You define schemas in Go, run go generate, and get fully typed queries, mutations, and migrations.
Think of it as schema-as-code: your database schema lives alongside your application code, versioned in Git, and the compiler enforces correctness.
Here’s the basic feel:

But it’s the patterns between the docs and raw SQL that made Ent viable at this scale. Here are six we discovered after a year of schema evolution.
1. PostgreSQL Trigram Search via Modify()
The problem: We needed typo-tolerant product search. ILIKE handles substrings but misses misspellings (“notbok” ≠ “notebook”), while full-text search is overkill for short names.

The solution:
PostgreSQL’s pg_trgm with similarity() gives fuzzy matching. Ent doesn’t support it natively, but Modify() lets us inject it cleanly—without losing type safety or query structure.

The key insight: Modify() is not a hack — it’s Ent’s deliberate escape hatch for database-specific features. You get parameterized queries (via Arg()), identifier quoting (via Ident()), and the full Ent query lifecycle around it.
2. Atomic Upserts with `OnConflictColumns`
The problem: Our messaging system tracks unread counts per conversation. Two messages arriving simultaneously could cause a classic read-modify-write race. Traditional fix: wrap in a transaction with SELECT … FOR UPDATE. But that’s two round-trips and a lock.
The solution: Ent’s OnConflictColumns maps to PostgreSQL’s INSERT … ON CONFLICT … DO UPDATE`, giving you an atomic single-statement upsert:

One pattern, two use cases: atomic increment and idempotent upsert.
3. Row-Level Locking for Payment Safety
The problem: When a payment webhook confirms an order, we update the order status, deduct donor balances, and trigger fulfillment all atomically. If two webhooks arrive for the same order (retries, network duplicates), we risk double-processing a payment.
The solution: Ent’s ForUpdate() maps directly to SELECT … FOR UPDATE, locking the row until the transaction commits:

ForUpdate() is available on every Ent query when you enable the sql/lock feature flag. Combined with context-based transactions (Pattern 4 below), it gives you the same pessimistic locking guarantees you’d get from raw SQL — without leaving the ORM.
4. Transactions via Context
The problem: Our checkout flow calls multiple service methods — create order, deduct balance, send notification. Each lives in a different service package. They all need to share one database transaction, but we don’t want them coupled to each other.

The solution: We inject the Ent transaction into context.Context:

5. DISTINCT ON and JSONB Filtering via Modify()
The problem: We need the latest audit record per batch — one row per batch_id. Standard SQL uses DISTINCT ON, but Ent has no built-in support for it.
The solution: Three lines with Modify():

The pattern is consistent: standard Ent for standard queries, Modify() for everything Postgres-specific. Whether it’s DISTINCT ON, JSONB operators, window functions, or lateral joins Modify() is your single escape hatch that doesn’t sacrifice the rest of Ent’s lifecycle.
6. Dynamic Predicate Composition
The problem: Given a list of (orderItemId, productId) pairs at runtime, find all matching audit records. The number of pairs varies. Building this with string concatenation in raw SQL is error-prone and risky.
The solution: Ent predicates are first-class values. Collect them in a slice, combine with Or():

Why We Never Write Raw SQL
All six patterns follow one rule: we stay within Ent no database/sql.
Modify() gives us raw SQL when needed, without losing type safety, transactions, or query structure. At our scale (100+ schemas), this matters—schema changes break at compile time, not in production.
We run additive-only migrations (WithDropColumn(false), WithDropIndex(false)), so the schema evolves safely.
Ent isn’t a cage — it’s a scaffold. Modify() is the escape hatch.
