skip to content

Why can a to-one link typed to the base class of a mapped hierarchy defeat deferred loading of that link?

level: middleimportance: should knowfreq 44%

answer

  1. a stand-in needs a class to be
  2. the key does not name the subtype
  3. read the target, or be base-typed
  4. type tests fail, virtual calls still work
  5. eager fallback shows up as per-row reads

basics

~20 s

A deferred stand-in has to be built as some class, and the key alone does not say which subtype the target is. So the layer either reads the target anyway or returns a base-typed stand-in that fails type tests.

solid answer

~50 s

Deferring a to-one link is cheap because the owning row already holds the target's key: the layer returns a stand-in carrying that key and reads the target only when it is touched. That needs a class for the stand-in, and when the link is typed to a hierarchy's base class the concrete subtype is only discoverable by reading the target's row or probing its child tables — the very read deferral avoids. Layers resolve this in one of three ways: load the target eagerly, reintroducing a per-owner read; return a stand-in typed to the base class, which stays deferred but fails subtype type tests and downcasts; or store the type beside the key on the owning side so the stand-in can be built concretely. Ordinary virtual calls still reach the subtype through a delegating stand-in — it is class-based branching that breaks.

go deeper

for a junior

Remember that a deferred reference is an object standing in for the real one, and that it has to be created as some class before the target is read.

for a middle

Explain why the concrete class is unknowable from the key alone, and what each of the layer's three responses costs.

for a senior

Diagnose it from evidence: per-row statements on a deferred link, or a type test failing on an object that the data says is a subtype, then fix by fetching explicitly or retyping the link.

for a principal

Treat every base-typed to-one link as a place where deferral or type fidelity is compromised, and account for that when deciding to map a hierarchy at all.

## Why deferral usually works, and what a hierarchy takes away A deferred to-one reference is normally cheap. The owning row already carries the target's key, so the layer can hand back a **stand-in**: an object of the target's type that holds the key and reads the target's row only when a member is touched. Nothing about the target has to be read to build it. That trick has a precondition: the layer must be able to **construct** the stand-in, and constructing it means picking a class for it. When the link is typed to the base class of a mapped hierarchy, the key alone does not say which subtype the target actually is. Where the marker lives on the target's row, the layer would have to read that row; where the class is inferred from which child table holds the key, it would have to probe those tables. Either way the answer is behind exactly the read that deferral existed to avoid. ## The choices a layer has, and what each costs - **Load it eagerly.** Correct types, no surprises — but the deferred link becomes a join or an extra statement per owning row, which is the N+1 shape when a list of owners is loaded. - **Return a stand-in typed to the base class.** Deferral is preserved, but the object's own class is the base type or a generated subclass of it, not the concrete subtype. Type tests against a subtype answer false and downcasts fail, even after the target has been read. - **Store the type alongside the key on the owning side.** Now the stand-in can be built concretely without touching the target. Some layers support this; it duplicates the marker and has to be kept consistent with it. There is no free option, which is why "a base-typed to-one link quietly stops being lazy" is such a common finding. ## What breaks in the code above it | symptom | why the hierarchy caused it | |---|---| | A type test against a subtype answers false | the stand-in's own class is the base type, not the subtype | | A downcast to a subtype fails | same reason; the concrete object sits behind the stand-in | | A visitor or double dispatch picks the base branch | branch selection is driven by the runtime class | | Equality that compares classes rejects an equal object | the stand-in's class differs from a directly loaded instance | | An unexplained extra statement per owner row | the layer chose eager loading rather than a wrongly typed stand-in | Note the asymmetry: in layers whose stand-in **delegates** to the loaded object, an ordinary virtual call still reaches the subtype's implementation once the target is read. It is reflection over the runtime class — type tests, casts, class-based branching, class-based serialization — that the stand-in defeats. Behaviour survives; identity of type does not. ## Diagnosing it 1. Look at the statements for a page that loads many owners. A per-owner select or a wide join on an association you declared deferred means the layer refused to defer it. 2. Look for code that branches on the target's type shortly after loading the owner. That code is either forcing the read or getting the wrong branch. 3. Compare an object reached through the link with the same object loaded directly by key: if one passes a type test and the other does not, you are holding a stand-in. ## What to do about it - **Prefer behaviour to type tests** on a polymorphic link. A method on the base type works through a delegating stand-in; a type test does not. - **Type the link concretely** where the model allows it. A link that genuinely points at one subtype should say so, and deferral comes back for free. - **Fetch the association explicitly** in the query that needs the concrete type, so the type is known when the objects arrive rather than discovered one row at a time. - **Keep polymorphic to-one links shallow.** Each one is a place where deferral, typing, or both are compromised, and the cost compounds when such links chain. - If the model needs the type before the target is read, consider carrying the type next to the key on the owning side deliberately, and accept that it must be maintained with the target's own marker.

  • If the stand-in is base-typed, why do ordinary method calls still behave correctly?
    In layers whose stand-in delegates, the call is forwarded to the loaded object, so the subtype's implementation runs. What the stand-in cannot fake is its own runtime class, so type tests, downcasts, class-based branching and class-based serialization see the base type instead of the subtype.
  • How would you spot this in a running system?
    Two symptoms. Either a statement per owning row (or a wide join) for an association you declared deferred, meaning the layer chose eager loading; or code that branches on the target's type taking the wrong branch, meaning you are holding a base-typed stand-in. Comparing with the same object loaded directly by key confirms it.
  • When is typing the link to a concrete class the right answer?
    Whenever the link genuinely points at one kind of thing. If the domain never puts another subtype behind that reference, declaring the base type buys nothing and costs deferral. Say what the link really points at, and the stand-in becomes constructible again.

saying these in an interview costs you the question

  • Thinks deferral only depends on whether the key column is nullable
  • Believes a stand-in always has the concrete subtype's class
  • Says a type test starts passing once the stand-in is initialized
  • Assumes eager loading here is free because it is one link
  • Blames the stand-in for wrong results from ordinary method calls