How a JPA save() Call Silently Overwrote My Data (and How to Fix It)
How a JPA save() Call Silently Overwrote My Data (and How to Fix It)
I recently encountered a subtle but dangerous issue in a Spring Boot + JPA application.
There were no errors, no exceptions, and no failed transactions — yet a database column
was silently overwritten with NULL.
If you are using Spring Data JPA and updating the same table from multiple endpoints, this is something you should be aware of.
The Simplified Scenario
We had two REST endpoints updating the same entity, but different fields. The code below is simplified and mangled for illustration.
Endpoint A – updates an external reference
EntityX entity = repository.findByKey(refId);
// update external reference
entity.setExternalRef("ABC123");
repository.save(entity);
Endpoint B – updates customer email
EntityX entity = repository.findByKey(refId);
// update email only
entity.setEmail("user@example.com");
repository.save(entity);
The assumption was that Endpoint B would only update the email field. Unfortunately, that assumption is incorrect.
The Problem: Silent Data Overwrite
What actually happened:
- Endpoint A correctly saved
externalRef - Endpoint B ran later
externalRefbecame NULL in the database- No error, no warning, no rollback
This behavior is extremely dangerous because it fails silently.
Why This Happens
In JPA, save(entity) does not perform a partial update.
It persists the entire state of the entity instance.
If a field is null in memory, JPA assumes you want it to be NULL
in the database.
This means JPA may generate SQL like:
UPDATE entity_x
SET email = ?,
external_ref = NULL
WHERE id = ?
Even if external_ref previously had a valid value.
When This Bug Typically Appears
- Multiple endpoints update the same table
- Each endpoint loads and saves the entity independently
- No optimistic locking is used
- Fields are nullable in the database
This is normal JPA behavior — not a race condition edge case.
How to Debug This Issue
1. Enable SQL logging
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
Check for unexpected column = NULL updates.
2. Log entity state before saving
log.debug("Saving entity: {}", entity);
You may be surprised which fields are null.
Correct Fixes (Recommended Order)
✅ Fix #1: Use Partial Update Queries (Best Practice)
@Modifying
@Query("""
update EntityX e
set e.email = :email
where e.key = :refId
""")
void updateEmail(String refId, String email);
This avoids loading the entity and guarantees other fields remain untouched.
✅ Fix #2: Enable Optimistic Locking
@Version
private Long version;
This prevents stale updates and fails fast instead of corrupting data.
⚠️ Fix #3: Defensive Re-Setting (Not Ideal)
Manually restoring unchanged fields before saving is error-prone and not recommended for complex entities.
🛑 Fix #4: Database NOT NULL Constraints
This prevents corruption at the database level but causes runtime failures instead of fixing the root issue.
Key Takeaways
save()persists the entire entity state- JPA does not protect you from overwriting data
- Silent data loss is worse than exceptions
- Partial updates or optimistic locking are the correct solutions
If you ever hear “we are only updating one field, it’s safe” — double check the code.
JPA will do exactly what you told it to do, not what you meant.
❤️ Support This Blog
If this post helped you, you can support my writing with a small donation. Thank you for reading.
Comments
Post a Comment