In a row-mapping data-access layer with no tracked set, how does a change to a loaded row reach the database?
answer
- no snapshot is kept
- nothing watches the object
- the write is a statement you send
- commit is not dirty checking
- forgetting it fails silently
basics
~20 sOnly through an UPDATE statement the code issues itself. The loaded object is a plain value with no link back to its row, so nothing detects the mutation, and forgetting the statement loses the change silently.
solid answer
~40 sA layer that maps rows by hand keeps no tracked set and no before-image of what it loaded, so there is no dirty checking to infer a write from. The object you hold is an ordinary value: mutating it changes memory and nothing else. To persist it you run a second statement — an `UPDATE` naming the columns and binding the key — inside the same transaction, and you check the affected-row count if you need to know the row was still there. The failure mode is the important part: omitting that statement produces no error at all, and committing does not help, because a commit makes durable the statements that were sent and never goes hunting for modified objects.
go deeper
Recall the shape: load, mutate in memory, then send your own UPDATE. Nothing between step two and step three happens on your behalf.
Explain why there is nothing to detect the change: no registry of loaded objects and no before-image to compare against, so the write cannot be inferred.
Point at the operational consequence — a missed write is silent, so the guard is review discipline and tests that re-read the row rather than asserting on the in-memory object.
Frame it as a trade: you give up inference and gain a code path where the statements the database runs are the statements in the source, which is worth real money on hot paths.
## Two shapes a data-access layer can take A **full mapper** keeps a *tracked set*: every object it loads inside a unit of work is registered, and the layer remembers what each object looked like when it arrived. Before the unit of work ends it compares the current state of each registered object against that remembered snapshot — **dirty checking** — and emits the UPDATE statements the comparison implies. You never write those statements. You mutate an object and the layer infers the write. A **row-mapping layer or query builder** keeps nothing. It sends a statement, walks the result, hands you objects built from the columns, and forgets them. There is no snapshot, no registry, no link at all between the object in your hand and the row it came from. That single absence is what the whole question turns on. ## So how does the change reach the database? Only because you state it. The path is explicit from end to end: 1. You run a SELECT and map each row into an object. 2. You change the object in memory. Nothing observes this — it is an ordinary in-memory mutation of an ordinary value. 3. You call the layer again with an UPDATE that names the columns you want written and binds the key. 4. You read the affected-row count if you care whether the row was still there to update. 5. You commit the transaction that both statements ran inside. Step 3 has no counterpart in a mapper-driven flow, and it is the whole difference. Forget it and nothing fails: no exception, no warning, no log line. The change is simply discarded when the object goes out of scope. Silent lost writes are the most common bug a team hits in its first weeks on an untracked layer, and they usually show up as *the field saves on one screen and not on another*. ## What the object you were handed actually is It is a plain value. Concretely that means: - **No identity.** Load the same row twice and you hold two distinct objects that can be edited apart and can disagree. - **No stand-ins.** Nothing is a lazy placeholder that will quietly fetch on access; what was selected is there and what was not selected is absent. - **No lifetime rules.** The object is valid after the transaction ends, safe to serialise, safe to hand to another thread, safe to cache — because nothing behind it is still live. - **No cascade.** Saving a parent writes exactly the parent's row; children are your statements to write. That is also the upside, and it is worth stating in an interview rather than only listing losses: the code you read is the work the database does. No flush at a moment you did not choose, no fetch firing behind a property read, no statement in the log that no line of code mentions. ## The comparison, concern by concern | Concern | Full mapper with a tracked set | Layer with no tracking | |---|---|---| | Making a change durable | Mutate the object; the layer emits the UPDATE | Issue the UPDATE yourself | | Which columns are written | Whatever the snapshot comparison found changed | Exactly the ones you name | | Same row loaded twice | Usually the same instance, from the identity map | Two independent objects | | Related rows | May be written by cascade | Separate statements you write | | Concurrency check | Often automatic through a version column | Your own version predicate and row-count check | | When statements are sent | At a flush the layer decides | At the call you make | ## What a good answer includes Say what is absent (the tracked set and the snapshot), then what follows from that absence (the write must be stated, and forgetting is silent), then one sentence of honest balance: the layer gives up inference and gets predictability. A candidate who describes the untracked layer purely as *the primitive option* has missed why teams choose it deliberately for read-heavy and set-shaped work. Two clarifications that come up in follow-ups. First, **transactions are unaffected** — an untracked layer still runs inside a transaction and still commits or rolls back as a unit; what it lacks is change detection, not atomicity. Second, **committing is not saving**. A commit makes durable whatever statements were actually sent; it never goes looking for modified objects. A candidate who says the commit will pick the change up has confused the transaction boundary with a flush, and in an untracked layer there is no flush to confuse it with.
- Does a layer with no tracked set still give you transactions?Yes. Atomicity, rollback and isolation come from the database and the connection, not from change tracking. Several hand-written statements still commit or roll back as one unit. What is missing is only the inference step that turns a mutated object into a statement; everything about the transaction boundary is unchanged.
- Why is a forgotten write worse here than a forgotten flush under a mapper?Under a mapper the change is registered and will be emitted at some flush, so forgetting an explicit call usually still works. Here nothing registered anything, so the omission is invisible: no exception, no statement in the log, and the object looks correct in memory until the next request re-reads the row.
- How do you write only the columns that actually changed?You decide, in code. Either always write the full set of updatable columns, which is simple and predictable, or compare the loaded values with the new ones yourself and build the statement from the difference. The layer will not do the comparison for you, so keep the rule uniform across the codebase rather than per-method.
A tracked set is like a shared document that saves as you type. A row-mapping layer hands you an emailed copy: edit it all you like, the original is untouched until you deliberately send a replacement.
saying these in an interview costs you the question
- Thinks committing the transaction saves modified objects
- Assumes the layer detects changes without a tracked set
- Expects a rollback because the write was silently skipped
- Believes an untracked layer cannot use transactions at all
- Treats two loads of one row as the same object