Skip to content

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):

  1. Model-level eager loading — declaring select = eager on 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.
  2. Domain-level load joins — the *relation term of the PdoSelectList / PdoSelectUnique wurblets. 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:

## relations
Address:
    relation = object,
    select   = eager

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 CLASSVARIABLES as a list of Joins (one LEFT JOIN per eager relation), assembled by DbModelWurblet.getEagerRelations() and emitted by ClassVariables. The runtime applies these joins to the standard by-id / select-all statements.
  • When another entity's relation generates a finder for this entity, PdoRelations appends each eager relation as a *relation load join to that generated PdoSelectList / 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:

// @wurblet findByName PdoSelectList name *Keys
// 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 = eager in 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 lazy and 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.