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
  • externalRef became 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

Popular posts from this blog

fixed: embedded-redis: Unable to run on macOS Sonoma

Copying MDC Context Map in Web Clients: A Comprehensive Guide

Reset user password for your own Ghost blog