javahibernatejpaoptimistic-lockingoptimistic-concurrency

Optimistic locking in a stateless application with JPA / Hibernate


I'm wondering what would be the best way to implement optimistic locking (optimistic concurrency control) in a system where entity instances with a certain version can not be kept between requests. This is actually a pretty common scenario but almost all examples are based on applications that would hold the loaded entity between requests (in a http session).

How could optimistic locking be implemented with as little API pollution as possible?

Constraints

The stack is Spring with JPA (Hibernate), if this should be of any relevance.

Problem using @Version only

In many documents it looks like all you need to do would be to decorate a field with @Version and JPA/Hibernate would automatically check versions. But that only works if the loaded objects with their then current version are kept in memory until the update changes the same instance.

What would happen when using @Version in a stateless application:

  1. Client A loads item with id = 1 and gets Item(id = 1, version = 1, name = "a")
  2. Client B loads item with id = 1 and gets Item(id = 1, version = 1, name = "a")
  3. Client A modifies the item and sends it back to the server: Item(id = 1, version = 1, name = "b")
  4. The server loads the item with the EntityManager which returns Item(id = 1, version = 1, name = "a"), it changes the name and persist Item(id = 1, version = 1, name = "b"). Hibernate increments the version to 2.
  5. Client B modifies the item and sends it back to the server: Item(id = 1, version = 1, name = "c").
  6. The server loads the item with the EntityManager which returns Item(id = 1, version = 2, name = "b"), it changes the name and persist Item(id = 1, version = 2, name = "c"). Hibernate increments the version to 3. Seemingly no conflict!

As you can see in step 6, the problem is that the EntityManager reloads the then current version (version = 2) of the Item immediately before the update. The information that Client B started editing with version = 1 is lost and the conflict can not be detected by Hibernate. The update request performed by Client B would have to persist Item(id = 1, version = 1, name = "b") instead (and not version = 2).

The automatic version check provided by JPA/Hibernate would only work if the instances loaded on the the initial GET request would be kept alive in some kind of client session on the server, and would be updated later by the respective client. But in a stateless server the version coming from the client must be taken into consideration somehow.

Possible solutions

Explicit version check

An explicit version check could be performed in a method of an application service:

@Transactional
fun changeName(dto: ItemDto) {
    val item = itemRepository.findById(dto.id)
    if (dto.version > item.version) {
        throw OptimisticLockException()
    }
    item.changeName(dto.name)
}

Pros

Cons

Forgetting the check could be prevented through an additional wrapper (ConcurrencyGuard in my example below). The repository would not directly return the item, but a container that would enforce the check.

@Transactional
fun changeName(dto: ItemDto) {
    val guardedItem: ConcurrencyGuard<Item> = itemRepository.findById(dto.id)
    val item = guardedItem.checkVersionAndReturnEntity(dto.version)
    item.changeName(dto.name)
}

A downside would be that the check is unnecessary in some cases (read-only access). But there could be another method returnEntityForReadOnlyAccess. Another downside would be that the ConcurrencyGuard class would bring a technical aspect to the domain concept of a repository.

Loading by ID and version

Entities could be loaded by ID and version, so that the conflict would show at load time.

@Transactional
fun changeName(dto: ItemDto) {
    val item = itemRepository.findByIdAndVersion(dto.id, dto.version)
    item.changeName(dto.name)
}

If findByIdAndVersion would find an instance with the given ID but with a different version, an OptimisticLockException would be thrown.

Pros

Cons

Updating with explicit version

@Transactional
fun changeName(dto: itemDto) {
    val item = itemRepository.findById(dto.id)
    item.changeName(dto.name)
    itemRepository.update(item, dto.version)
}

Pros

Cons

Update version property explicitly on mutation

The version parameter could be passed to mutating methods which could internally update the version field.

@Entity
class Item(var name: String) {
    @Version
    private version: Int

    fun changeName(name: String, version: Int) {
        this.version = version
        this.name = name
    }
}

Pros

Cons

A variant of this pattern would be to set the version directly on the loaded object.

@Transactional
fun changeName(dto: ItemDto) {
    val item = itemRepository.findById(dto.id)
    it.version = dto.version
    item.changeName(dto.name)
}

But that would expose the version directly expose for reading and writing and it would increase the possibility for errors, since this call could be easily forgotten. However, not every method would be polluted with a version parameter.

Create a new Object with the same ID

A new object with the same ID as the object to be update could created in the application. This object would get the version property in the constructor. The newly created object would then be merged into the persistence context.

@Transactional
fun update(dto: ItemDto) {
    val item = Item(dto.id, dto.version, dto.name) // and other properties ...
    repository.save(item)
}

Pros

Cons

Question

How would you solve it and why? Is there a better idea?

Related


Solution

  • The server loads the item with the EntityManager which returns Item(id = 1, version = 1, name = "a"), it changes the name and persist Item(id = 1, version = 1, name = "b"). Hibernate increments the version to 2.

    That's a misuse of the JPA API, and the root cause of your bug.

    If you use entityManager.merge(itemFromClient) instead, the optimistic locking version would be checked automatically, and "updates from the past" rejected.

    One caveat is that entityManager.merge will merge the entire state of the entity. If you only want to update certain fields, things are a bit messy with plain JPA. Specifically, because you may not assign the version property, you must check the version yourself. However, that code is easy to reuse:

    <E extends BaseEntity> E find(E clientEntity) {
        E entity = entityManager.find(clientEntity.getClass(), clientEntity.getId());
        if (entity.getVersion() != clientEntity.getVersion()) {
            throw new ObjectOptimisticLockingFailureException(...);
        }
        return entity;
    }
    

    and then you can simply do:

    public Item updateItem(Item itemFromClient) {
        Item item = find(itemFromClient);
        item.setName(itemFromClient.getName());
        return item;
    }
    

    depending on the nature of the unmodifiable fields, you may also be able to do:

    public Item updateItem(Item itemFromClient) {
        Item item = entityManager.merge(itemFromClient);
        item.setLastUpdated(now());
    }
    

    As for doing this in a DDD way, the version checking is an implementation detail of the persistence technology, and should therefore occur in the repository implementation.

    To pass the version through the various layers of the app, I find it convenient to make the version part of the domain entity or value object. That way, other layers do not have to explicitly interact with the version field.