web-dev-qa-db-fra.com

Verrouillage optimiste dans une application sans état avec JPA / Hibernate

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?

Contraintes

  • Le système est développé sur la base des principes de conception pilotée par domaine.
  • Système client/serveur
  • Les instances d'entité ne peuvent pas être conservées entre les demandes (pour des raisons de disponibilité et d'évolutivité).
  • Les détails techniques doivent polluer le moins possible l'API du domaine.

La pile est Spring avec JPA (Hibernate), si cela doit être pertinent.

Problème d'utilisation de @Version Uniquement

Dans 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:

  1. Client A charge l'élément avec id = 1 Et obtient Item(id = 1, version = 1, name = "a")
  2. Le client B charge l'élément avec id = 1 Et obtient Item(id = 1, version = 1, name = "a")
  3. Le client A modifie l'élément et le renvoie au serveur: Item(id = 1, version = 1, name = "b")
  4. Le serveur charge l'élément avec le 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.
  5. Le client B modifie l'élément et le renvoie au serveur: Item(id = 1, version = 1, name = "c").
  6. Le serveur charge l'élément avec le 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.

Solutions possibles

Vérification de version explicite

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

  • La classe de domaine (Item) n'a pas besoin d'un moyen de manipuler la version de l'extérieur.
  • La vérification de version ne fait pas partie du domaine (à l'exception de la propriété de version elle-même)

Inconvénients

  • facile à oublier
  • Le champ de version doit être public
  • la vérification automatique de la version par le framework (au plus tard possible) n'est pas utilisée

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.

Chargement par ID et version

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

  • impossible d'oublier gérer la version
  • 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

  • Pollution de l'API du référentiel
  • 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

Mise à jour avec une version explicite

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

Avantages

  • toutes les méthodes de mutation de l'entité ne doivent pas être polluées par un paramètre de version

Inconvénients

  • L'API du référentiel est polluée par le paramètre technique version
  • Des méthodes update explicites contrediraient le modèle "unité de travail"

Mettre à jour la propriété de version explicitement lors d'une mutation

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

  • inoubliable

Inconvénients

  • fuites de détails techniques dans toutes les méthodes de domaine en mutation
  • facile à oublier
  • Il est non autorisé de changer directement l'attribut de version des entités gérées.

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.

Créer un nouvel objet avec le même ID

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

  • cohérent pour toutes sortes de modifications
  • impossible d'oublier l'attribut de version
  • les objets immuables sont faciles à créer
  • pas besoin de charger d'abord l'objet existant dans de nombreux cas

Inconvénients

  • L'ID et la version en tant qu'attributs techniques font partie de l'interface des classes de domaine
  • La création de nouveaux objets empêcherait l'utilisation de méthodes de mutation ayant une signification dans le domaine. Il existe peut-être une méthode 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.
  • Conflits avec le modèle d '"unité de travail".

Question

Comment le résoudriez-vous et pourquoi? Y a-t-il une meilleure idée?

En relation

21
deamon

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.

0
ckedar