When a query returns a row the tracked set already holds with an unsaved edit, which values does the caller get back?
answer
- rows in, instances out
- map consulted after the read
- held edit is not overwritten
- explicit re-read is the escape hatch
basics
~20 sNormally the held object, edit intact - not the columns the database just sent. The layer reconciles each returned row against its identity map and hands back the instance it already holds, dropping the freshly read values.
solid answer
~50 sThe rows come back from the database as columns, but before the caller sees objects the layer reconciles each row against the identity map. For a key it already holds it returns **the held instance**, and in most layers the freshly read column values for that row are simply discarded — so a pending in-memory edit survives and the caller sees the edited state rather than what the database sent. That is deliberate: the alternative would silently overwrite work you have not written yet. Two exceptions matter: the caller can ask explicitly for a re-read that overwrites the object, and a projection into a transfer shape never consults the map, so it shows the database's values. Whether the layer flushes pending changes before running the query — and therefore whether the database even sees your edit while evaluating the predicate — is a separate question about flush timing.
code
pseudocode · 13 lineso = load(Order, 7) // read, placed in the identity map
o.status = "SHIPPED" // edit held in memory, nothing written yet
// assume the layer did not write pending changes before this query
rows = query(Order).where(status = "NEW") // predicate evaluated in the database
// row 7 comes back with status "NEW" in its columns
// the map already holds 7, so that instance is returned as-is
contains(rows, o) == true // same instance, not a copy
o.status == "SHIPPED" // held edit survived the read
refresh(o) // explicit re-read: overwrites the edit
o.status == "NEW"go deeper
Remember the order of events: the database answers the predicate, then the layer swaps in objects it already holds. So a query result can legitimately show values the database has never seen.
Walk the reconciliation steps and say why the held edit wins — overwriting it would make lost updates the default — then name the explicit re-read and the projection path as the two ways round it.
Recognise the symptom in the wild: a screen or a test where a tracked object and a projection disagree, and a 'refresh' implemented as a re-query that cannot possibly work. Fix it at the read path, not by clearing everything.
Decide the policy: which read paths in the system are tracked at all. Serving read-heavy paths as untracked projections removes the whole reconciliation surprise, at the cost of two code paths over one model.
## Reconciliation, step by step A query is evaluated by the database, so what comes back is rows of columns. Turning those rows into objects is where the identity map intervenes. For each returned row a typical layer does roughly this: 1. Read the identifier columns and form the map key (type plus identifier). 2. **Look the key up in the identity map.** If an object is already held for it, put that object in the result list and, in most layers, discard the columns just read for that row. 3. If nothing is held, materialise a new object from the columns, place it in the map, and put it in the result list. 4. Return the list of objects — a mixture of freshly built ones and ones the set was already holding. Step 2 is the whole answer. The result list is not a picture of what the database returned; it is *the set's own objects*, selected by a question the database answered. ## Which values win | Situation | What the caller sees | |---|---| | Row not held yet | The database's column values, in a new object | | Row held, no local edit | The held object; its values equal what was read, so nothing looks odd | | Row held, local edit pending | The **held object with the edit**, not the columns just read | | Explicit re-read or refresh requested | The database's values, overwriting the held object's state | | Projection into a transfer shape | The database's values; the map is not consulted at all | The third line is the one that surprises people, and the fifth is what makes it visible: run a query for objects and a projection over the same rows in the same set, and they can disagree, because only one of them passes through the map. ## Why layers behave this way Silently overwriting an unsaved edit with values read a moment later would make lost updates the default. The tracked set's contract is that your edits stand until you write or discard them, so reconciliation keeps the instance and drops the duplicate read. It also keeps the one-object rule intact: if the query built a second object for a row the set already holds, the set would have two candidates to write at commit. The cost is that a query result is not evidence of the current database state for rows you already touched, which is exactly what a debugging session is often trying to establish. ## Where it bites - A test edits an object, re-queries, and asserts the *old* value is gone from the results — it is not, because the object came back as edited. - A screen shows one value from a tracked object and a different one from a projection built for the same screen. - Code "refreshes" by re-running the finder and is puzzled that nothing changed; re-running a query cannot overwrite a held object, only an explicit re-read of that object can. - A predicate looks wrong: an object comes back that no longer matches the filter, or one that now matches is missing. That is about *when* pending changes reach the database before the query runs, which is flush timing's territory, not the map's — but the two show up together and are easy to confuse. ## The two escape hatches - **Force a re-read of the object.** Every layer offers a way to say "go and read this row again and overwrite what I hold". It costs a statement and it throws away pending edits on that object, which is the point. - **Discard the object from the set** so the next read genuinely materialises it again. Discarding is not the same as re-reading: it also drops any pending edit, and the object you were holding becomes detached rather than updated. Data-access layers differ here: some let you request a re-read per query and reconcile every returned row against the database's values, others only per object, and query builders that keep no tracked set at all simply hand back whatever the columns said, every time. ## Saying it well Separate the three actors: the database answered a predicate, the layer decided *which object* to hand you, and your unsaved edit decided *what state* that object is in. A candidate who keeps those apart can explain the surprise in one sentence — the query chose the rows, the map chose the instances, and your edit chose the values — and then name the explicit re-read and the projection path as the two ways to see the database's own answer.
- Why does a projection into a transfer shape disagree with the tracked object for the same row?Because a projection never becomes a tracked object. Its columns are read and handed over as plain values, with no map lookup and no reconciliation, so it reports what the database currently holds. The tracked object reports what the set holds, including edits not yet written. Both are correct answers to different questions.
- If reconciliation keeps the held instance, how do you ever see the database's current values for a row you already hold?Ask for it explicitly: re-read that object so its state is overwritten from the database, or discard it from the set so the next read materialises it fresh. Alternatively read the row as a projection, which bypasses the map entirely. Re-running the original query does neither.
- Does reconciliation apply to rows reached through an association rather than a top-level query?Yes in most layers. When a lazily loaded association resolves, its rows go through the same map lookup, so an element you already hold comes back as the instance you hold rather than as a second object. That is why an edited child appears already-edited the first time you walk the parent's collection.
saying these in an interview costs you the question
- Says a query always returns the database's current values
- Thinks re-running a finder refreshes objects the set already holds
- Expects the query to build a second object for an already-held row
- Assumes a projection and a tracked object for one row must agree
- Believes reconciliation overwrites pending edits by default