Skip to content
AITroveRead. Build. Understand.
Make this comfortable

JPA detached merge: prevent partial requests from erasing stored fields

Last updated: 5 Oct 20264 min read
tutorial
IntermediateBy AITrove Editorial

EntityManager.merge copies detached state, so a sparse API payload must not masquerade as a complete entity snapshot.

A missing field becomes a write

A client sends only a receipt status and ID. If the controller constructs a detached ReceiptEntity with the other fields left null, then calls repository save, JPA may merge the nulls into a managed copy. That makes a patch request behave like a destructive replacement. New-state detection decides persist versus merge; it does not make a partial entity safe.

Load then change allowed fields

Keep a request DTO separate from the entity. In one tenant-scoped transaction, query the row using both tenant and receipt ID, verify the expected revision, and update only fields the command permits. Dirty checking writes the changed managed entity at flush. Put invariant checks before mutation. The attached entity stays owned by this persistence context; do not hand it to a background thread or serialize it directly into an API response.

Make conflicts visible

Test a sparse request with an existing non-null customer reference and total. The update should preserve both. Test a stale revision and a cross-tenant ID; neither may change the row. A narrow JPQL update can be better for a single column if it includes the tenant and version predicates, but bulk updates bypass managed-entity synchronization.

Implementation contract

Java
@Transactional
public ReceiptView changeStatus(UUID tenantId, UUID receiptId, long expectedRevision, String nextStatus) {
    ReceiptEntity receipt = repository.findByTenantIdAndReceiptId(tenantId, receiptId).orElseThrow();
    if (receipt.getRevision() != expectedRevision) throw new OptimisticLockingFailureException("stale receipt");
    receipt.changeStatus(nextStatus);
    return ReceiptView.from(receipt);
}

Cost and verification

Load-and-change usually needs a SELECT plus an UPDATE. It preserves field-level intent and authorization; a predicate update can use one SQL statement when its lost-update contract is explicit.

Common Mistakes

  • Do not bind a partial JSON object directly onto a managed entity.
  • Do not call merge on a sparse entity and expect null fields to be ignored.
  • Do not check ownership outside the transaction that performs the change.

Read next

Spring Data JPA assigned IDs: tell save when an entity is new, Spring Data JPA bulk update: the managed entity can still hold the old value, Spring tenant command transaction: keep state, event and replay record together, Spring JDBC versioned tenant update: inspect the affected row count, Spring JPA entity lifecycle: managed changes and detached objects.

spring
spring-data
jpa
jpa-detached-merge-patch-risk
Storage details