When a partially populated object is copied into a tracked set, how are its nulls, absent collection elements and linked objects treated?
answer
- replace, not patch
- an omitted field is an assigned null
- the collection is made to match
- the copy travels only where cascade says
basics
~20 sA copy-in overwrites the target field by field, so a null in the incoming object stores a null, a collection is made to match the one handed in, and links are followed only where the mapping cascades the copy.
solid answer
~50 sCopy-in is a replace, not a patch. The layer walks the mapped fields of the object you hand it and assigns each value onto the tracked instance, so a field left null because your input never carried it is stored as null and wipes what was there. Collections behave the same way one level up: the tracked association is made to look like the one you passed, so an omitted element is unlinked — whether its row is then deleted or merely left disconnected depends on what the mapping says about children that lose their parent. Links to other objects are only followed where the association is configured to `cascade` the copy; a detached child on a non-cascading link is ignored or rejected, depending on the layer. The safe habit: load the row and apply only the fields the caller actually sent.
go deeper
Remember that copying an object in replaces the stored values with the ones you supply, empty ones included. A field missing from your input is not preserved; it is stored as null.
Explain the field-by-field assignment and the collection-level replace, then name the two things that decide the rest: what the mapping cascades, and whether rows that lose their parent are deleted or merely disconnected.
Describe the write path you would build: load the row inside the unit of work, apply the fields the request genuinely carried, and let change detection produce a narrow update instead of handing a half-filled object to a whole-object copy.
Own the contract question. Whether an update means replace-the-whole-thing or apply-this-delta is an interface decision, and the persistence path should be chosen to match it rather than discovering the mismatch later as missing data.
Copying a detached object's state into a tracked set is a **replace**, not a patch. That single sentence explains most of the surprises, and most of the data loss, that this mechanism produces. The layer walks the mapped fields of the object you hand it and assigns each value onto the tracked instance for that key. It has no way to know which of those values you meant and which merely defaulted, because an unset field and a deliberately cleared field look identical in memory. ## Scalar fields and nulls If the object you hand in carries a null for a column that currently holds a value, the assignment stores null and the old value is gone at the next flush. There is no heuristic that skips empty values — skipping nulls is patch semantics, and if you want it you write it yourself in a mapping step. This turns a very ordinary-looking pipeline into data loss: - An update request arrives carrying three fields the user actually edited. - A transfer model is populated from it, so the other fifteen mapped fields are null or zero. - That object is copied in, and fifteen columns are wiped. The request looked partial; the write was total. Nothing in the layer complained, because from its point of view it faithfully stored what it was given. ## Collections Collections follow the same logic one level up: the tracked association is made to look like the collection you handed in. An element present in the tracked collection but absent from yours is unlinked. Whether the row behind that element is then **deleted** or merely left disconnected is not a property of the copy-in — it is decided by the mapping: - If the mapping says children that lose their parent should be deleted, the unlinked row is removed. - If it does not, the row survives with a dangling or nulled reference, which is often worse than either intended outcome because nothing points at it any more. - If the incoming collection was never populated at all — a common accident when a transfer model omits it — an empty collection is still a collection, and the layer reads it as "make this association empty". ## Links to other objects The copy does not travel everywhere it can reach. It travels where the mapping says to **cascade** it: | Situation | What happens to the linked object | |---|---| | Association cascades the copy, child is detached | the child's state is copied onto its own tracked instance | | Association cascades the copy, child is new | it is brought in with the parent | | Association does not cascade, child is detached | the layer ignores it, or refuses the untracked reference — layers differ | | Association does not cascade, child is already tracked | the link is set; the child's own values come from wherever it was edited | The failure mode here is the mirror of the null one: a child object you fully expected to be saved silently is not, because nobody configured the link to carry the copy. It is worth reading cascade settings as part of reviewing any write path that hands a whole object graph to the layer. ## What to do instead The safest habit is to keep partial inputs away from whole-object copies entirely: 1. **Load the row inside the unit of work.** One read, by key, in the same piece of work that will write. 2. **Apply only what the caller actually sent.** If the protocol can distinguish "field absent" from "field set to empty", preserve that distinction all the way down to the assignment; if it cannot, decide explicitly which one absence means and document it. 3. **Let change detection produce the write.** Having edited a tracked object, you need no copy-in at all — the flush writes exactly the fields that changed. Where a whole-object copy-in genuinely is the right tool — a full-replacement update, an import that owns every column — make that explicit in the contract, and make sure the object being copied really is complete, ideally by building it from a full read rather than from a request body. ## The contract question underneath Ultimately this is not a mapper question but an interface one. "Update this thing" is ambiguous: it can mean *here is the new state of the whole thing* or *here is what changed*. Whole-object copy-in implements the first. Loading and applying a delta implements the second. Data is lost whenever the write path implements one and the caller believes the other, so the useful discipline is to name which one you are offering before you decide how to persist it.
- Why is a copy-in a poor fit for a partial update request?Because it assigns every mapped field, a copy-in reads 'field not supplied' as 'set to null'. A partial update means 'leave everything else alone', so the two contracts are opposites. Load the row and apply only the fields the request genuinely carried instead.
- What decides whether an element dropped from a collection is deleted or only unlinked?The mapping, not the copy. Making the association match what you handed in at minimum breaks the link; whether the now-unreferenced row is deleted depends on whether the mapping asks for children that lose their parent to be removed. Otherwise it survives, disconnected.
- Does the copy reach objects on the other side of a link?Only where the association cascades it. Otherwise the layer either ignores the linked object or refuses a detached reference, so a child you expected to be written can silently not be. That is why cascade settings deserve a look whenever a write path hands over a whole graph.
saying these in an interview costs you the question
- Thinks a copy-in only touches the fields that were set
- Assumes a null in the input means leave the stored value alone
- Builds the incoming object from a partial payload and copies it in
- Expects children on a non-cascading link to be written too
- Believes an element dropped from a collection is always deleted from its table