Je me demande quelle serait la meilleure façon d'implémenter le verrouillage optismitique (contrôle de concurrence optimiste) dans un système où les instances d'entité avec une certaine version ne peuvent pas être conservées entre les requêtes. Il s'agit en fait d'un scénario assez courant, mais presque tous les exemples sont basés sur des applications qui détiendraient l'entité chargée entre les demandes (dans une session http).
Comment mettre en œuvre un verrouillage optimiste avec le moins de pollution API possible?
La pile est Spring avec JPA (Hibernate), si cela doit être pertinent.
@Version
UniquementDans de nombreux documents, il semble que tout ce que vous devez faire serait de décorer un champ avec @Version
Et JPA/Hibernate vérifierait automatiquement les versions. Mais cela ne fonctionne que si les objets chargés avec leur version actuelle sont conservés en mémoire jusqu'à ce que la mise à jour modifie la même instance.
Que se passerait-il lors de l'utilisation de @Version
Dans une application sans état:
id = 1
Et obtient Item(id = 1, version = 1, name = "a")
id = 1
Et obtient Item(id = 1, version = 1, name = "a")
Item(id = 1, version = 1, name = "b")
EntityManager
qui renvoie Item(id = 1, version = 1, name = "a")
, il change le name
et persiste Item(id = 1, version = 1, name = "b")
. Hibernate incrémente la version à 2
.Item(id = 1, version = 1, name = "c")
.EntityManager
qui renvoie Item(id = 1, version = 2, name = "b")
, il change le name
et persiste Item(id = 1, version = 2, name = "c")
. Hibernate incrémente la version à 3
. Apparemment pas de conflit! Comme vous pouvez le voir à l'étape 6, le problème est que EntityManager recharge la version actuelle (version = 2
) De l'élément immédiatement avant la mise à jour. Les informations que le client B a commencé à modifier avec version = 1
Sont perdues et le conflit ne peut pas être détecté par Hibernate. La demande de mise à jour effectuée par le client B devrait à la place persister Item(id = 1, version = 1, name = "b")
(et non version = 2
).
La vérification automatique de la version fournie par JPA/Hibernate ne fonctionnerait que si les instances chargées sur la demande GET initiale seraient maintenues en vie dans une sorte de session client sur le serveur et seraient mises à jour ultérieurement par le client respectif. Mais sur un serveur sans état, la version provenant du client doit être prise en compte d'une manière ou d'une autre.
Une vérification de version explicite pourrait être effectuée dans une méthode d'un service d'application:
@Transactional
fun changeName(dto: ItemDto) {
val item = itemRepository.findById(dto.id)
if (dto.version > item.version) {
throw OptimisticLockException()
}
item.changeName(dto.name)
}
Avantages
Item
) n'a pas besoin d'un moyen de manipuler la version de l'extérieur.Inconvénients
Oublier la vérification pourrait être évité grâce à un wrapper supplémentaire (ConcurrencyGuard
dans mon exemple ci-dessous). Le référentiel ne retournerait pas directement l'élément, mais un conteneur qui appliquerait la vérification.
@Transactional
fun changeName(dto: ItemDto) {
val guardedItem: ConcurrencyGuard<Item> = itemRepository.findById(dto.id)
val item = guardedItem.checkVersionAndReturnEntity(dto.version)
item.changeName(dto.name)
}
Un inconvénient serait que la vérification est inutile dans certains cas (accès en lecture seule). Mais il pourrait y avoir une autre méthode returnEntityForReadOnlyAccess
. Un autre inconvénient serait que la classe ConcurrencyGuard
apporterait un aspect technique au concept de domaine d'un référentiel.
Les entités peuvent être chargées par ID et version, de sorte que le conflit s'affiche au moment du chargement.
@Transactional
fun changeName(dto: ItemDto) {
val item = itemRepository.findByIdAndVersion(dto.id, dto.version)
item.changeName(dto.name)
}
Si findByIdAndVersion
trouverait une instance avec l'ID donné mais avec une version différente, un OptimisticLockException
serait jeté.
Avantages
version
ne pollue pas toutes les méthodes de l'objet domaine (bien que les référentiels soient également des objets domaine)Inconvénients
findById
sans version serait de toute façon nécessaire pour le chargement initial (lorsque l'édition commence) et cette méthode pourrait être facilement utilisée accidentellement@Transactional
fun changeName(dto: itemDto) {
val item = itemRepository.findById(dto.id)
item.changeName(dto.name)
itemRepository.update(item, dto.version)
}
Avantages
Inconvénients
version
update
explicites contrediraient le modèle "unité de travail"Le paramètre de version pourrait être transmis aux méthodes de mutation qui pourraient mettre à jour en interne le champ de version.
@Entity
class Item(var name: String) {
@Version
private version: Int
fun changeName(name: String, version: Int) {
this.version = version
this.name = name
}
}
Avantages
Inconvénients
Une variante de ce modèle serait de définir la version directement sur l'objet chargé.
@Transactional
fun changeName(dto: ItemDto) {
val item = itemRepository.findById(dto.id)
it.version = dto.version
item.changeName(dto.name)
}
Mais cela exposerait la version exposée directement pour la lecture et l'écriture et augmenterait la possibilité d'erreurs, car cet appel pourrait être facilement oublié. Cependant, toutes les méthodes ne seraient pas polluées avec un paramètre version
.
Un nouvel objet avec le même ID que l'objet à mettre à jour pourrait être créé dans l'application. Cet objet obtiendrait la propriété version dans le constructeur. L'objet nouvellement créé serait ensuite fusionné dans le contexte de persistance.
@Transactional
fun update(dto: ItemDto) {
val item = Item(dto.id, dto.version, dto.name) // and other properties ...
repository.save(item)
}
Avantages
Inconvénients
changeName
qui devrait effectuer une certaine action uniquement sur les modifications, mais pas sur le paramètre initial du nom. Une telle méthode ne serait pas appelée dans ce scénario. Peut-être que cet inconvénient pourrait être atténué avec des méthodes d'usine spécifiques.Comment le résoudriez-vous et pourquoi? Y a-t-il une meilleure idée?
Pour éviter les modifications simultanées, nous devons garder une trace de la version de l'élément qui est modifiée, quelque part.
Si l'application était dynamique, nous aurions la possibilité de conserver ces informations sur le côté serveur, éventuellement en session, bien que ce ne soit pas le meilleur choix.
Dans une application sans état, cette information devra aller jusqu'au client et revenir avec chaque demande de mutation.
Donc, IMO, si empêcher les modifications simultanées est une exigence fonctionnelle, alors avoir des informations sur la version de l'élément dans les appels d'API de mutation ne pollue pas l'API, cela la rend complète.