Eager Relations¶
Tentackle offers two distinct ways to pull a related object into the same SELECT as its parent instead of
firing a follow-up query (the classic N+1 problem):
- Model-level eager loading — declaring
select = eageron a relation in the model. This is a persistence property of the relation: the related object is fetched whenever the parent is loaded by a generic select. - Domain-level load joins — the
*relationterm of thePdoSelectList/PdoSelectUniquewurblets. This is a domain decision made per finder method, independent of the model.
The two look similar but solve different problems and have very different scope. The most important thing to understand is that model-level eager loading never cascades, whereas load joins are explicit and may chain to any depth you ask for.
1. Model-level eager loading (select = eager)¶
In a relation line you can choose the fetch strategy:
The available strategies are lazy (the default — fetched on first access), eager, always and embedded
(see Model Definition → select).
With eager, the related object is loaded together with its parent via a LEFT OUTER JOIN rather than on
first access.
What "eager" actually applies to¶
Eager loading is wired into the generic selects — the methods the persistence layer generates for every
entity: select(id), selectAll(), and the relation finders that other entities generate to load this
entity (selectByXxx). Concretely:
- The eager joins are baked into the entity's
CLASSVARIABLESas a list ofJoins (oneLEFT JOINper eager relation), assembled byDbModelWurblet.getEagerRelations()and emitted byClassVariables. The runtime applies these joins to the standard by-id / select-all statements. - When another entity's relation generates a finder for this entity,
PdoRelationsappends each eager relation as a*relationload join to that generatedPdoSelectList/PdoSelectUnique, so the eager objects ride along in the relation select as well.
In other words, "eager" is a property of how the entity is generically read. It is not a cascade instruction, and it does not magically change handwritten queries.
Eager never cascades — and why¶
This is the crucial rule. The optimized eager join is generated only one level deep.
getEagerRelations() walks the entity's
eager relations and abandons the whole join optimization (returns nothing, falling back to follow-up
selects) if either of these holds:
- a subclass of the entity declares an eager relation, or
- any direct component of the eagerly-joined entity itself declares an eager relation.
So an eager relation whose target also has eager relations does not produce a chain of nested
LEFT JOINs. The chain is intentionally broken.
The reason is performance. A naive transitively-cascading eager strategy is the classic way to destroy a
relational application: each additional eager level multiplies into the join, every parent row is fanned out
across the cross-product of all its descendants, the row count explodes, and a query that was meant to fetch
"one object and its address" ends up dragging half the database through a single statement. Long LEFT JOIN
chains are slow to plan, slow to execute, and slow to de-duplicate on the client. By capping eager loading at a
single level, Tentackle keeps the generic selects predictable: at worst, one parent plus a bounded set of
directly-joined relations.
If you find yourself wanting eager loading to cascade, that is a signal that you want a load join in a specific finder (next section), not a model-wide eager flag.
When to use it¶
Reach for select = eager only when a related object is almost always needed together with its parent and
the relation is shallow and bounded (a 1:1 link to a small satellite row, a short component list). It is a
blunt, global instrument: every generic read of the entity pays for it. For everything else, prefer lazy
(the default) and add targeted load joins where a particular access path needs them.
2. Domain-level load joins (*relation)¶
The second mechanism lives entirely in the domain layer. When you write a finder with
PdoSelectList / PdoSelectUnique, prefixing a relation with * eagerly loads it in that
one query:
// chained / filtered joins are allowed:
// @wurblet load PdoSelectList id *invoice*lines
// @wurblet load PdoSelectList id *invoice*lines|no:<:10
This is domain-related, not model-related. Key differences from model eager:
| Aspect | Model select = eager |
Load join *relation |
|---|---|---|
| Where declared | In the model (relation line) | In a finder anchor in the persistence implementation |
| Scope | Global — affects every generic read | Local — affects only that one finder |
| Cascading | Never — capped at one level | Explicit — you chain *a*b*c to whatever depth you need |
| Filtering | No | Yes — *lines |no:<:10 loads a subset |
| Granularity | All-or-nothing for the entity | Tuned per access path |
Because a load join is scoped to a single finder, it lets a particular domain operation say "for this use
case I need the invoice, its lines, and their currencies in one round-trip" without imposing that cost on every
other reader of the entity. Filtered joins additionally let you load only part of an aggregate (which forces
the result immutable unless you pass --mutable, since a
partial aggregate must not be saved back).
See PdoSelectList and PdoSelectUnique → Load joins for the full
grammar, join consolidation, immutability rules, and the --cursor constraint.
Choosing between the two¶
- The relation is small, shallow, and needed on virtually every read of the parent ⇒
select = eagerin the model. - Only certain queries need the related data, or you need to follow a deeper / filtered path, or you want to
avoid burdening other readers ⇒ leave the relation
lazyand add a*load join in the specific finder.
When in doubt, default to lazy and add load joins where profiling shows an N+1 problem. It is far easier to
add an eager load join to one finder than to discover, months later, that a model-wide eager flag is quietly
inflating every select of a central entity.
Related Documentation¶
PdoSelectListandPdoSelectUnique— the wurblets that implement load joins, including the full*relationgrammar and filtered joins.- Tentackle Persistence Wurblets — the module overview and wurblet catalogue.
- Model Definition →
select— the relationselectstrategies (lazy,eager,always,embedded). - PDO — Persistent Domain Objects — aggregates, components,
immutability, and the
JoinedSelectmachinery these loads build on.