160

How does @Version annotation work in JPA?

I found various answers whose extract is as follows:

JPA uses a version field in your entities to detect concurrent modifications to the same datastore record. When the JPA runtime detects an attempt to concurrently modify the same record, it throws an exception to the transaction attempting to commit last.

But I am still not sure how it works.


Also as from the following lines:

You should consider version fields immutable. Changing the field value has undefined results.

Does it mean that we should declare our version field as final?

ROMANIA_engineer
  • 54,432
  • 29
  • 203
  • 199
Amit
  • 33,847
  • 91
  • 226
  • 299
  • 1
    All it does is check/update the version in every update query: `UPDATE myentity SET mycolumn = 'new value', version = version + 1 WHERE version = [old.version]`. If someone updated the record, `old.version` will no longer match the one in the DB and the where clause will prevent the update from happening. The 'rows updated' will be `0`, which JPA can detect to conclude that a concurrent modification happened. – Stijn de Witt Mar 07 '17 at 09:20
  • Side note: If all your database queries happen within transactions and you use serializable isolation level you don't need this type of optimistic locking. Furthermore, @Version will not protect you from lost updates in case the modification spans multiple session/transactions as would be the case for a GET followed by PUT for some entity in HTTP REST. You should consider implementing ETag and If-Match for that. – masterxilo May 06 '23 at 00:22

5 Answers5

232

But still I am not sure how it works?

Let's say an entity MyEntity has an annotated version property:

@Entity
public class MyEntity implements Serializable {    

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @Version
    private Long version;

    //...
}

On update, the field annotated with @Version will be incremented and added to the WHERE clause, something like this:

UPDATE MYENTITY SET ..., VERSION = VERSION + 1 WHERE ((ID = ?) AND (VERSION = ?))

If the WHERE clause fails to match a record (because the same entity has already been updated by another thread), then the persistence provider will throw an OptimisticLockException.

Does it mean that we should declare our version field as final

No but you could consider making the setter protected as you're not supposed to call it.

Pascal Thivent
  • 562,542
  • 136
  • 1,062
  • 1,124
  • 4
    Don't rely on auto-unboxing it to `long` or just calling `longValue()` on it. You need explicit `null` checks. Explicitly initializing it yourself I would not do as this field is supposed to be managed by the ORM provider. I think it gets set to `0L` on first insert into the DB, so if it's set to `null` this tells you this record has not been persisted yet. – Stijn de Witt Oct 19 '15 at 14:00
  • Will this prevent creating an entity being (tried to) created more than once? I mean suppose a JMS topic message triggers creation of an entity and there are multiple instances of an application listens to the message. I just want to avoid the unique constraint violation error.. – Manu Jan 19 '16 at 06:28
  • 1
    Sorry, I realize that this is an old thread, but does @Version also take into consideration dirty cache in clustered environments? – Shawn Eion Smith Jan 20 '16 at 00:14
  • @ussmith That's a very good question! Never thought about it. I assume if the cache is part of/managed by the JPA provider (.e.g. Hibernate L1/L2 cache) that yes, it should take it into account. But I have no evidence to back this up. – Stijn de Witt Feb 20 '17 at 08:20
  • So a public getVersion() is necessary so that the (version = ?) in WHERE clause can get a value? – Man Shen Nov 04 '18 at 05:30
  • @ManShen Judging by the answer and comments, calling the getter method should be done when you need the value for some logic, as the value itself is updated by the container (you are not required to set or update it). From Markus answer, i believe your are supposed to explicitly initialize it to avoid the null pointer exception, thus if the container doesn't do that for you. – basamoahjnr Mar 20 '19 at 07:40
  • 6
    If using Spring Data JPA, it may be better to use a wrapper `Long` rather than a primitive `long` for the `@Version` field, because `SimpleJpaRepository.save(entity)` will likely treat a null version field as an indicator that the entity is new. If the entity is new (as indicated by version being null), Spring will call `em.persist(entity)`. But if version has a value, then it will call `em.merge(entity)`. This is also why a version field that's declared as a wrapper type should be left uninitialized. – rdguam Jul 24 '19 at 03:01
  • Hi, I have a situation where I introduced @Version on an existing field and there were null values in that field (old records). So, if an old record is modified and the annotation is trying to increment. As my column type is Integer, it is trying to get .intValue() and gets a null-pointer. Please advise, TIA. – – Prakash Palnati Jun 02 '21 at 05:40
48

Although @Pascal answer is perfectly valid, from my experience I find the code below helpful to accomplish optimistic locking:

@Entity
public class MyEntity implements Serializable {    
    // ...

    @Version
    @Column(name = "optlock", columnDefinition = "integer DEFAULT 0", nullable = false)
    private long version = 0L;

    // ...
}

Why? Because:

  1. Optimistic locking won't work if field annotated with @Version is accidentally set to null.
  2. As this special field isn't necessarily a business version of the object, to avoid a misleading, I prefer to name such field to something like optlock rather than version.

First point doesn't matter if application uses only JPA for inserting data into the database, as JPA vendor will enforce 0 for @version field at creation time. But almost always plain SQL statements are also in use (at least during unit and integration testing).

halfer
  • 19,824
  • 17
  • 99
  • 186
G. Demecki
  • 10,145
  • 3
  • 58
  • 58
  • I call it **`u_lmod`** (user last modified) where **user can be a human or defined (automated) process/entity/app** (so storing additionally `u_lmod_id` could make sense as well). (It is in my view a very basic business-meta-attribute). It is typically storing its value as DATE(TIME) (meaning infos about year...millis) in UTC (if the timezone "location" of the editor is important to store then with timezone). additionally very useful for DWH sync scenarios. – Andreas Covidiot Aug 25 '15 at 10:06
  • 10
    I think bigint instead of an integer should be used in db type to match a long java type. – Dmitry Feb 01 '17 at 17:03
  • Good point Dmitry, thanks, although when it comes to versioning IMHO it doesn't really matter. – G. Demecki Feb 02 '17 at 09:06
  • Hi, I have a situation where I introduced @Version on an existing field and there were null values in that field (old records). So, if an old record is modified and the annotation is trying to increment. As my column type is Integer, it is trying to get .intValue() and gets null pointer. Please advise, TIA. – Prakash Palnati Jun 02 '21 at 05:39
  • @PrakashPalnati just update your database to set version = 0 where version is null? – Sebastiaan van den Broek Aug 16 '21 at 05:29
  • Nope, that doesn’t work in JPA – Prakash Palnati Jan 09 '22 at 15:37
12

Every time an entity is updated in the database the version field will be increased by one. Every operation that updates the entity in the database will have appended WHERE version = VERSION_THAT_WAS_LOADED_FROM_DATABASE to its query.

In checking affected rows of your operation the jpa framework can make sure there was no concurrent modification between loading and persisting your entity because the query would not find your entity in the database when it's version number has been increased between load and persist.

stefanglase
  • 10,296
  • 4
  • 32
  • 42
  • Not via JPAUpdateQuery it doesn't. So "Every operation that updates the entity in the database will have appended WHERE version = VERSION_THAT_WAS_LOADED_FROM_DATABASE to its query" is not true. – Pedro Borges Apr 27 '17 at 17:44
2

Version used to ensure that only one update in a time. JPA provider will check the version, if the expected version already increase then someone else already update the entity so an exception will be thrown.

So updating entity value would be more secure, more optimist.

If the value changes frequent, then you might consider not to use version field. For an example "an entity that has counter field, that will increased everytime a web page accessed"

uudashr
  • 119
  • 1
  • 4
1

Just adding a little more info.

JPA manages the version under the hood for you, however it doesn't do so when you update your record via JPAUpdateClause, in such cases you need to manually add the version increment to the query.

Same can be said about updating via JPQL, i.e. not a simple change to the entity, but an update command to the database even if that is done by hibernate

Pedro

Pedro Borges
  • 1,568
  • 1
  • 14
  • 25