How to update the @Version field when executing a bulk update through Spring Data JPA
Section 4.10 of the JPA 2.0 specification explicitly states:
Bulk update maps directly to a database update operation, bypassing optimistic locking checks. Portable applications must manually update the value of the version column, if desired, and/or manually validate the value of the version column.
Generally speaking bulk updates and deletes pretty much bypass a lot of functionality that is applied by the persistence provider that you might be used to when simply saving an entity. Besides optimistic locking this includes the persistence provider managed cascading of persistence operations.
In order to use optimistic locking, you need a version
column first.
Now, if the bulk update statement does not increment the version
column, a Lost Update anomaly can occur:
Now, to avoid the Lost Update, you just need to increment the version
column.
You can do it with any type of query.
JPQL
int updateCount = entityManager
.createQuery(
"update Post " +
"set " +
" status = :newStatus," +
" version = version + 1 " +
"where " +
" status = :oldStatus and " +
" lower(title) like :pattern")
.setParameter("oldStatus", PostStatus.PENDING)
.setParameter("newStatus", PostStatus.SPAM)
.setParameter("pattern", "%spam%")
.executeUpdate();
Criteria API
CriteriaBuilder builder = entityManager
.getCriteriaBuilder();
CriteriaUpdate<Post> update = builder
.createCriteriaUpdate(Post.class);
Root<Post> root = update.from(Post.class);
Expression<Boolean> wherePredicate = builder
.and(
builder.equal(
root.get("status"),
PostStatus.PENDING
),
builder.like(
builder.lower(root.get("title")),
"%spam%"
)
);
Path<Short> versionPath = root.get("version");
Expression<Short> incrementVersion = builder
.sum((short) 1, versionPath);
update
.set(root.get("status"), PostStatus.SPAM)
.set(versionPath, incrementVersion)
.where(wherePredicate);
int updateCount = entityManager
.createQuery(update)
.executeUpdate();
Native SQL
UPDATE post
SET
status = 2,
version = version + 1
WHERE
status = 0 AND
lower(title) LIKE '%spam%'
Once you increment the version
column, the Lost Update will be prevented in the concurrent transactions that loaded the previous versions of the entities updated by the bulk statement: