skip to content

When a native statement's rows are mapped to objects, do those objects join the layer's tracked set?

level: middleimportance: should knowfreq 58%

answer

  1. the mapping target decides
  2. entity in, transfer model out
  3. already-loaded instance usually wins
  4. two objects, one row, last write wins
  5. partial columns plus tracking equals data loss

basics

~10 s

The mapping target decides. Rows mapped to an entity type usually enter the tracked set and are reconciled with any instance already loaded for that identity; transfer models and scalars stay outside it.

solid answer

~50 s

The mapping target decides. Mapped to an **entity type**, the row normally goes through the layer's ordinary load path: the instance is tracked, and if an object for that identity is already loaded, most layers hand back the **existing instance** and discard the freshly read column values rather than overwriting your in-memory changes. Mapped to a **transfer model** or a **scalar**, nothing is tracked — you get plain values that no flush will ever write back. A layer with no tracked set at all returns plain objects in every case. Two hazards follow. First, a tracked native result is a live object: modify it and a flush writes it, even if you thought of the statement as a read. Second, where a raw result is *not* reconciled against what is already loaded, you can hold **two objects for one row** with different values, and the later write wins.

go deeper

for a junior

Remember the rule of thumb: rows mapped to a mapped entity type behave like loaded objects, rows mapped to a transfer model or a scalar are plain data that will never be written back.

for a middle

Explain reconciliation by identity — why an already-loaded instance is usually returned instead of the freshly read values — and why layers legitimately differ on whether raw results are tracked at all.

for a senior

Diagnose the duplicate-instance symptom: an edit that reverts or a value that disagrees across one screen, caused by two objects for one row and last-write-wins between them.

for a principal

Set the convention that removes the class of bug: reads map to transfer models by default, entity mapping is the exception with the full column set, and the choice is visible at the call site.

## The question behind the question "Is it tracked?" is really "will something I do to this object later turn into an `UPDATE` I did not write?" — and, symmetrically, "can this object be silently different from another object representing the same row?" Both answers turn on whether the mapped result entered the layer's **unit of work**, the set of objects it watches for changes and reconciles by identity. ## What the mapping target decides | Target | Enters the tracked set | Written back on flush | Reconciled against an already-loaded instance | |---|---|---|---| | Mapped entity type | usually yes | yes, if modified | usually yes, in a layer that keeps identity | | Transfer model | no | never | no such notion | | Scalar or tuple | no | never | no such notion | The entity row is the interesting one, and the honest statement is that **layers differ**: some route a native entity result through exactly the same load path as an object-level query, some deliberately keep raw results outside the tracked set unless you ask, and some let you choose per statement. The safe habit is to know which yours does rather than to assume, because both designs are defensible and both are in the wild. ## Reconciliation, when the layer does it In a layer that keeps loaded objects unique per identity, a native row for an identity already present usually resolves to the **existing instance**, and the newly read column values are dropped. This is not a bug; it protects your in-flight changes from being clobbered by a background read. But it means: - A native statement written specifically to see fresh values may return the **stale** in-memory object anyway. - Two callers within the same unit of work see the same instance, so a change by one is visible to the other immediately. - To genuinely re-read a row you must ask the layer to refresh it, or work in a unit of work that never loaded it. ## When there is no reconciliation Where the raw result bypasses identity reconciliation, you can hold **two objects for the same row**: the one loaded earlier and the one the native statement produced. They can disagree; each may be tracked or not; and if both are written, the later write wins with no complaint. Symptoms are the classic ones — an edit that "didn't save", a value that reverts, a count that disagrees with a list on the same screen. ## Partial rows, a related trap A native statement that selects only some of a type's columns and asks for that type can produce an object whose remaining fields hold defaults. If such an object is tracked, change detection compares it against a snapshot that also came from the partial row, and a flush may write the defaults out. Two safe rules: select the full declared column set when you want entities, and map to a transfer model whenever you only need some columns. ## Layers without a tracked set A row-mapping layer or a query builder has no unit of work at all. Every result is a plain object, nothing is watched, nothing is reconciled, and nothing is written unless you write it. That is simpler and completely predictable — and it is why the question only arises above a mapper. ## How to choose deliberately 1. **Do you intend to modify what comes back?** If no, map to a transfer model. That single choice removes tracking overhead, removes accidental writes and removes the duplicate-instance question in one move. 2. **If yes**, select the full column set for the type, and accept that the object is live from the moment it lands. 3. **If you need genuinely fresh values for a row already loaded**, plan for it: refresh the instance explicitly, or run the read in a unit of work that has no prior copy. 4. **Never mix intentions in one statement** — a result that is half report and half editable object is a defect waiting for its first concurrent edit. ## What to say in an interview The crisp version: the mapping target decides, entity results usually become live tracked objects and are reconciled by identity where the layer keeps identity, everything else is plain data — and the failure mode to name out loud is two in-memory objects for one row, with last-write-wins deciding the outcome.

  • Why might a native statement written to get fresh values still return stale data?
    Because in a layer that keeps one instance per identity, the row resolves to the object already loaded and the freshly read values are discarded to protect in-flight changes. You need an explicit refresh of that instance, or a unit of work that never loaded the row.
  • How does mapping to a transfer model remove several problems at once?
    It leaves the result outside the tracked set, so nothing is watched for changes, nothing is written back accidentally, no identity reconciliation applies, and selecting a subset of columns is normal rather than dangerous. You give up the ability to modify and save what came back.

saying these in an interview costs you the question

  • Assumes native results are always read-only whatever the target
  • Assumes native results are always tracked whatever the target
  • Expects a native read to refresh an already-loaded object's values
  • Modifies a mapped entity from a native read without expecting a write
  • Selects a few columns into an entity type and lets it be tracked