Optimistic locking in a stateless application with JPA / Hibernate
All explanations and suggestions made here were very helpful, but since the final solutions differs a bit, I think it is worth sharing it.
Manipulating the version
directly didn't work properly and conflicts with the JPA spec, so it was no option.
The final solution is the explicit version check + automatic version checking by JPA Hibernate. The explicit version check is performed at the application layer:
@Transactional
fun changeName(dto: ItemDto) {
val item = itemRepository.findById(dto.id)
rejectConcurrentModification(dto, item)
item.changeName(dto.name)
}
To reduce repetition, the actual check happens in a separate method:
fun rejectConcurrentModification(dto: Versioned, entity: Versioned) {
if (dto.version != entity.version) {
throw ConcurrentModificationException(
"Client providing version ${dto.version} tried to change " +
"entity with version ${entity.version}.")
}
}
Entities and DTOs both implement the Versioned
interface:
interface Versioned {
val version: Int
}
@Entity
class Item : Versioned {
@Version
override val version: Int = 0
}
data class ItemDto(override val version: Int) : Versioned
But pulling version
from both and pass it to rejectConcurrentModification
would work equally well:
rejectConcurrentModification(dto.version, item.verion)
The obvious downside of the explicit check at the application layer is that it could be forgotten. But since the repository must provide a way to load an entity without a version anyway, adding the version to the find
method of the repository wouldn't be safe either.
The upside of the explicit version check at the application layer is that it doesn't pollute the domain layer except the version
needs to be readable from the outside (by implementing the Versioned
interface). Entity or repository methods, which are both part of the domain, are not polluted with version
parameters.
That the explicit version check is not performed at the latest possible point in time doesn't matter. If between this check and the final update on the database another user would modify the same entity, then the automatic version check by Hibernate would become effective, since the version loaded at the beginning of the update request is still in memory (on the stack of the changeName
method in my example). So, the first explicit check would prevent a concurrent modification happend between the begin of the edit on the client and the explicit version check. And the automatic version check would prevent a concurrent modification between the explicit check and the final update on the database.
When you load the record from DB to process the update request , you have to configure that loaded instance to have the same version supplied by the client. But unfortunately when an entity is managed , its version cannot be changed manually as required by the JPA spec.
I try to trace the Hibernate source codes and do not notice there are any Hibernate specific feature that can by-passed this limitation. Thankfully, the version checking logic is straightforward such that we can check it by ourself. The entity returned is still managed which means unit of work pattern can still be applied to it :
// the version in the input parameter is the version supplied from the client
public Item findById(Integer itemId, Integer version){
Item item = entityManager.find(Item.class, itemId);
if(!item.getVersoin().equals(version)){
throws new OptimisticLockException();
}
return item;
}
For the concern about API will be polluted by the version
parameter, I would model entityId
and version
as a domain concept which is represented by a value object called EntityIdentifier
:
public class EntityIdentifier {
private Integer id;
private Integer version;
}
Then have a BaseRepository
to load an entity by EntityIdentifier
. If the version
in EntityIdentifier
is NULL, it will be treated as the latest version. All repositories of other entities will extend it in order to reuse this method :
public abstract class BaseRepository<T extends Entity> {
private EntityManager entityManager;
public T findById(EntityIdentifier identifier){
T t = entityManager.find(getEntityClass(), identifier.getId());
if(identifier.getVersion() != null && !t.getVersion().equals(identifier.getVersion())){
throws new OptimisticLockException();
}
return t;
}
Note: This method does not mean loading the state of the entity at an exact version since we are not doing event sourcing here and will not store the entity state at every version. The state of the loaded entity will always be the latest version , the version in EntityIdentifier is just for handling the optimistic locking.
To make it more generic and easily to use , I will also define an EntityBackable
interface such that the BaseRepository
can load the backed entity of anything (e.g. DTO) once they implement it.
public interface EntityBackable{
public EntityIdentifier getBackedEntityIdentifier();
}
And add the following method to BaseRepository
:
public T findById(EntityBackable eb){
return findById(eb.getBackedEntityIdentifier());
}
So at the end, ItemDto
and updateItem()
application service looks like:
public class ItemDto implements EntityBackable {
private Integer id;
private Integer version;
@Override
public EntityIdentifier getBackedEntityIdentifier(){
return new EntityIdentifier(id ,version);
}
}
@Transactional
public void changeName(ItemDto dto){
Item item = itemRepository.findById(dto);
item.changeName(dto.getName());
}
To summarise , this solution can :
- Unit of work pattern still valid
- Repository API will not populated with version parameter
- All technical details about controlling the version are encapsulated inside
BaseRepository
, so no technical details are leaks into the domain.
Note:
setVersion()
is still need to be exposed from the domain entity.But I am okay with it as the entity get from the repository is managed which means there are no effect on the entity even developers callssetVersion()
. If you really don't want developers to callsetVersion()
. You can simply to add an ArchUnit test to verify that it can only be called from theBaseRepository
.
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.