Spring Data JPA New Entity Detection: How It Determines Object State

🚦 The Core Decision: persist or merge?

Internally, when you call repository.save(entity), Spring Data JPA decides between two JPA operations:

  • entityManager.persist(entity) — used for new entities
  • entityManager.merge(entity) — used for existing entities

The decision hinges on a single method:

This method determines whether the entity should be treated as new or not.

🧠 Default Behavior: How isNew() Works

The default implementation comes from JpaMetamodelEntityInformation, which uses the presence of a @Version field and the type of the ID to make the decision.

Here’s a simplified version of the logic:

  • If there’s no @Version field or it’s a primitive type, Spring falls back to AbstractEntityInformation.isNew()
  • If it’s a wrapper type (e.g. Long), it checks whether the version field is null

Now let’s look at AbstractEntityInformation.isNew():

So, if your entity has:

  • A wrapper ID (Long, UUID, etc.), Spring treats it as new only if ID is null
  • A primitive ID (long, int), it must be zero to be considered new

This works fine when IDs are auto-generated. But what if they aren’t?


🧩 What If You Set the ID Yourself?

If you assign the ID manually and skip @GeneratedValue, Spring Data will often think the entity is not new, even if it’s the first time you’re saving it. That’s because the ID is non-null and thus fails the “is new” check.

To solve this, you can implement the Persistable<T> interface in your entity:

This tells Spring explicitly whether an entity is new. Now you’re in full control. Spring will use your logic in isNew() instead of relying on ID or version heuristics.


🧪 Why Does This Matter?

Let’s peek under the hood at how save() works in SimpleJpaRepository:

Here’s the gotcha:

If Spring thinks the entity is not new (because the ID is set), it uses merge().
Even if the entity doesn’t actually exist in the DB, merge() will:

  • Try to fetch it first (wasted query)
  • Then do an insert if not found (extra overhead)

This can be expensive, and even dangerous, depending on your context. Implementing Persistable gives you control to avoid that.


🧭 TL;DR Summary
ScenarioResult
@Id with @GeneratedValueHandled automatically
@Id manually assigned, no @VersionMay be treated as existing
@Version used (Long, not long)Checked for null to decide
Implements PersistableYou decide what’s “new”

📘 Further Reading

🎯 Final Thoughts

Knowing how isNew() works isn’t just academic—it’s essential for:

  • Writing efficient data access code
  • Avoiding subtle bugs
  • Building custom ID strategies

If you’re manually assigning IDs or doing clever things with detached entities, this is a concept you must have under your belt.

See you in the next post—maybe something like “persist() vs merge(): What’s really happening under the hood?” 🔧


Kuni
Kuni

Hi, I’m a developer based in South Korea. With years of experience in the tech industry, I am passionate about creating meaningful solutions and continually learning in this ever-evolving field.

I believe in the importance of leading a healthy and balanced economic life, and I aim to share insights, ideas, and practical tips to help others achieve the same. Through this blog, I hope to connect with like-minded individuals, exchange valuable knowledge, and grow together.

Let’s explore, learn, and build a thriving life together!

Let me know if you'd like further adjustments! 😊