I'm trying to implement locking on a JPA/Hibernate application, to detect when a client is trying to push changes on an out-of-date version of an entity.
I've choose to expose dto's (representing a subset of the domain) over REST services. At the moment, I can easily detect concurrent transactions updates, but I can not make it work to detect "old" entities updates. I'll explain:
- #1 2 concurrent transactions manipulating the same entity, each attached to their entity manager, are correctly protected against dirty reads (the last to commit gets an OptimisticLockingException)
#2 2 concurrent users, manipulating the same entity with their frontend and committing two different, non-concurrent transactions, do NOT get any lock exception. Thats because, using DTO's, the update part of the code is something like that:
- start a transaction
- get the persisted entity to update from manager
- copy what is relevant from the dto to that entity
- commit
... but nothing (JPA, Hibernate or whatever) never checks for the consistency between the dto's version and the entity's one... (ps: trying to set the @Version field with the version given in the dto, as specified by JPA, lead to weird results)
Based on what I've seen and lots of debugging and docs read, I've ended up writing a code like that:
abstract class AbstractBusinessService {
private static final Logger LOG = LoggerFactory.getLogger(AbstractBusinessService.class);
protected final void checkEntityVersion(Long givenVersion, VersionedEntity<?> entity) {
if (givenVersion == null) {
LOG.warn("no version have been provided. unable to check for concurrent modification");
} else if (entity == null) {
LOG.warn("the given entity is null");
} else if (entity.getVersion() != givenVersion.longValue()) {
throw new LockingException("The persistent entity " + entity.getClass().getName() + "#" + entity.getId()
+ " has a newer version than expected (" + givenVersion + " vs. " + entity.getVersion() + ")");
}
}
}
... invoked before every "update" operation... It obviously works like a charm, but adds some complexity on the business layer, for a topic that is purely persistence-related and should not be visible on the business layer...
Am I right in the sense that this is a DIY thing and nothing is available out-of-the-box to implement #2? Am I missing something?? How do you address that problem in your development?
Thank you very much for reading,
SP
EDIT : as per indicated in comments, the best way seems to
- get a persistent entity from context (A)
- create a new, clean (detached/transient) entity (B)
- copy every properties of (A) to (B)
- set every properties in (B) with what comes from the DTO (conversion may be needed)
- merge (B) --> get LockException if @Version mismatch