skip to content

How does a @Version property change Spring Data's is-new detection, and when does it take precedence over the @Id check?

level: middleimportance: should knowfreq 38%

answer

  1. @Version null = new, non-null = existing
  2. version beats id check (layer 2)
  3. Persistable beats version (layer 1)
  4. primitive version ignored -> falls back to id
  5. use boxed Long version to fix assigned ids

basics

~20 s

If the entity has a non-primitive @Version field (e.g. Long version), Spring Data considers it new when the version is null, instead of checking the id. A primitive version type is ignored and it falls back to the id check.

solid answer

~40 s

When an entity declares a `@Version` property of a non-primitive type (`Long`, `Integer`, `Instant`…), Spring Data JPA's `JpaMetamodelEntityInformation.isNew` uses the **version** rather than the id: a `null` version means new (INSERT/persist), a non-null version means existing (UPDATE/merge). This is useful when the id is assigned manually, because the version is still null on a fresh object even though the id isn't. The precedence rule matters: version-based detection only kicks in for a *wrapper* version type; if the `@Version` field is a **primitive** (`long`, `int`), Spring Data can't distinguish 'unset' from 0, so it ignores it and falls back to the default id-null check. So to fix assigned-id inserts via versioning you must use a boxed version type. Persistable.isNew(), if implemented, overrides both.

code

java · 15 lines
java
@Entity
class Order {
    @Id
    private UUID id = UUID.randomUUID(); // assigned in memory -> id never null

    @Version
    private Long version;                 // null on a fresh object -> isNew() == true
    // NOTE: 'private long version;' (primitive) would be IGNORED for is-new,
    // and Spring Data would fall back to the id check (always 'existing') -> bug.

    private String customer;
}

// Fresh object: version == null -> persist (INSERT), version set to 0 after insert.
// Reloaded object: version != null -> merge (UPDATE) with optimistic-lock check.

go deeper

for a junior

Know that a null @Version can mark an entity as new.

for a middle

State the null-version rule and the primitive-type exception that reverts to the id check.

for a senior

Place @Version in the full precedence order and use it to solve assigned-id inserts without Persistable.

for a principal

Weigh version-based detection vs Persistable for a domain, and reason about the same rule across JPA/JDBC/Mongo.

## Recap: the detection hierarchy Spring Data JPA decides `isNew` in `JpaMetamodelEntityInformation` using a small priority order: 1. If the entity implements **`Persistable<ID>`**, use its `isNew()` (highest priority — a dedicated `JpaPersistableEntityInformation` is chosen). 2. Else, if there's a usable **`@Version`** attribute, use the version. 3. Else, fall back to the **@Id** null/zero check (from `AbstractEntityInformation`). This question is about layer 2. ## What @Version normally does `@Version` (JPA optimistic locking) marks a field that the provider increments on every update and checks on write to detect concurrent modification. Types allowed by JPA include `int/Integer`, `long/Long`, `short/Short`, `Timestamp`. A brand-new, never-persisted entity has a **null** version (for wrapper types); after the first insert the provider sets it (typically 0). ## How Spring Data reuses it for is-new Spring Data notices that 'version is null' is an even better 'this was never saved' signal than 'id is null', because it stays true even when you assign ids yourself. So `JpaMetamodelEntityInformation.isNew`: - If a version attribute is present **and its type is not primitive**, returns `version == null`. - Otherwise (no version, or a **primitive** version type) it calls `super.isNew(entity)` — the id-based check. ## The primitive gotcha A primitive `long version` defaults to `0` and can never be null, so Spring Data cannot tell 'never saved' (0) from 'saved and at version 0' (also 0). Rather than guess, it **ignores the primitive version** and reverts to the id check. Consequence: if you were relying on versioning to fix assigned-id inserts, a primitive version type silently reintroduces the bug. **Always use a boxed `@Version` type (`Long`, `Integer`, `Instant`) if you want version-driven is-new detection.** ## When this helps The pattern shines with manually-assigned ids: `@Id private UUID id = UUID.randomUUID();` is never null, so the id check would always say 'existing'. Add `@Version private Long version;` and a fresh object reports `version == null -> isNew == true -> INSERT`, while a loaded object has a non-null version and updates — no extra SELECT and no `Persistable` boilerplate. ## Store notes Spring Data JDBC and R2DBC also support `@Version`-based optimistic locking and use the same 'null version = new' logic for entity-state detection. MongoDB likewise. The primitive-vs-wrapper reasoning is the same everywhere: only a nullable version can distinguish unset from zero. ## Key terms - **Optimistic locking** — concurrency control that detects (rather than prevents) conflicting updates by comparing a version stamp on write. - **JpaMetamodelEntityInformation** — the JPA-specific EntityInformation that implements this layered isNew logic.

  • Why does a primitive @Version type disable version-based is-new detection?
    A primitive long/int can never be null and defaults to 0, so Spring Data can't distinguish 'never persisted' from 'persisted at version 0'. To stay safe it ignores the primitive version and reverts to the @Id-based check.

saying these in an interview costs you the question

  • Saying @Version always overrides the id check — it only does so for non-primitive version types; a primitive version is ignored.
  • Claiming @Version is only about optimistic locking and has no effect on insert/update decisions.
  • Believing @Version detection overrides Persistable.isNew() — it's the reverse: Persistable wins.

context