Annotation Java - JPA - @Version

118

Comment fonctionne l' @Versionannotation dans JPA?

J'ai trouvé diverses réponses dont l'extrait est le suivant:

JPA utilise un champ de version dans vos entités pour détecter les modifications simultanées du même enregistrement de banque de données. Lorsque l'environnement d'exécution JPA détecte une tentative de modification simultanée du même enregistrement, il lève une exception à la transaction qui tente de valider en dernier.

Mais je ne sais toujours pas comment cela fonctionne.


Aussi à partir des lignes suivantes:

Vous devez considérer les champs de version immuables. La modification de la valeur du champ a des résultats non définis.

Cela signifie-t-il que nous devrions déclarer notre champ de version comme final?

Yatendra Goel
la source
1
Tout ce qu'il fait est de vérifier / mettre à jour la version dans toutes les requêtes de mise à jour: UPDATE myentity SET mycolumn = 'new value', version = version + 1 WHERE version = [old.version]. Si quelqu'un a mis à jour l'enregistrement, old.versionne correspondra plus à celui de la base de données et la clause where empêchera la mise à jour de se produire. Les «lignes mises à jour» seront 0, que JPA peut détecter pour conclure qu'une modification simultanée s'est produite.
Stijn de Witt

Réponses:

189

Mais je ne sais toujours pas comment cela fonctionne?

Disons qu'une entité MyEntitya une versionpropriété annotée :

@Entity
public class MyEntity implements Serializable {    

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @Version
    private Long version;

    //...
}

Lors de la mise à jour, le champ annoté avec @Versionsera incrémenté et ajouté à la WHEREclause, quelque chose comme ceci:

UPDATE MYENTITY SET ..., VERSION = VERSION + 1 WHERE ((ID = ?) AND (VERSION = ?))

Si la WHEREclause ne correspond pas à un enregistrement (car la même entité a déjà été mise à jour par un autre thread), le fournisseur de persistance lèvera un OptimisticLockException.

Cela signifie-t-il que nous devons déclarer notre champ de version comme final

Non, mais vous pourriez envisager de protéger le passeur car vous n'êtes pas censé l'appeler.

Pascal Thivent
la source
5
J'ai eu une exception de pointeur nul avec ce morceau de code, car Long initialise comme null, devrait probablement être initialisé explicitement avec 0L.
Markus
2
Ne comptez pas sur le déballage automatique longou simplement longValue()sur l' appel . Vous avez besoin de nullcontrôles explicites . Je ne ferais pas de l'initialisation explicite vous-même, car ce champ est censé être géré par le fournisseur ORM. Je pense qu'il est réglé 0Llors de la première insertion dans la base de données, donc s'il est réglé sur nullcela, cela vous indique que cet enregistrement n'a pas encore été conservé.
Stijn de Witt du
Cela empêchera-t-il la création (tentée) de créer une entité plus d'une fois? Je veux dire, supposons qu'un message de rubrique JMS déclenche la création d'une entité et qu'il existe plusieurs instances d'une application qui écoute le message. Je veux juste éviter l'erreur de violation de contrainte unique ..
Manu
Désolé, je me rends compte qu'il s'agit d'un ancien thread, mais @Version prend-il également en compte le cache sale dans les environnements en cluster?
Shawn Eion Smith
3
Si vous utilisez Spring Data JPA, il peut être préférable d'utiliser un wrapper Longplutôt qu'une primitive longpour le @Versionchamp, car SimpleJpaRepository.save(entity)il traitera probablement un champ de version nulle comme un indicateur que l'entité est nouvelle. Si l'entité est nouvelle (comme indiqué par la version étant nulle), Spring appellera em.persist(entity). Mais si la version a une valeur, elle appellera em.merge(entity). C'est également pourquoi un champ de version déclaré en tant que type de wrapper doit être laissé non initialisé.
rdguam
33

Bien que la réponse @Pascal soit parfaitement valide, d'après mon expérience, je trouve le code ci-dessous utile pour réaliser un verrouillage optimiste:

@Entity
public class MyEntity implements Serializable {    
    // ...

    @Version
    @Column(name = "optlock", columnDefinition = "integer DEFAULT 0", nullable = false)
    private long version = 0L;

    // ...
}

Pourquoi? Car:

  1. Le verrouillage optimiste ne fonctionnera pas si le champ annoté avec @Versionest accidentellement défini sur null.
  2. Comme ce champ spécial n'est pas nécessairement une version commerciale de l'objet, pour éviter une erreur, je préfère nommer ce champ à quelque chose comme optlockplutôt que version.

Premier point n'a pas d' importance si application utilise uniquement JPA pour insérer des données dans la base de données, en tant que fournisseur JPA appliquera 0pour le @versionterrain au moment de la création. Mais presque toujours des instructions SQL simples sont également utilisées (au moins pendant les tests unitaires et d'intégration).

G. Demecki
la source
Je l'appelle u_lmod(utilisateur modifié en dernier lieu) où l' utilisateur peut être un processus / entité / application humain ou défini (automatisé) (donc le stockage supplémentaire u_lmod_idpourrait également avoir un sens). (C'est à mon avis un méta-attribut métier très basique). Il stocke généralement sa valeur sous la forme DATE (HEURE) (ce qui signifie des informations sur l'année ... millis) en UTC (si le fuseau horaire "emplacement" de l'éditeur est important à stocker, alors avec le fuseau horaire). en outre très utile pour les scénarios de synchronisation DWH.
Andreas Dietrich
7
Je pense que bigint au lieu d'un entier devrait être utilisé dans le type db pour correspondre à un type java long.
Dmitry
Bon point Dmitry, merci, bien que quand il s'agit de versioning IMHO cela n'a pas vraiment d'importance.
G.Demecki
9

Chaque fois qu'une entité est mise à jour dans la base de données, le champ de version est augmenté de un. Chaque opération qui met à jour l'entité dans la base de données sera ajoutée WHERE version = VERSION_THAT_WAS_LOADED_FROM_DATABASEà sa requête.

En vérifiant les lignes affectées de votre opération, le framework jpa peut s'assurer qu'il n'y a pas eu de modification simultanée entre le chargement et la persistance de votre entité car la requête ne trouverait pas votre entité dans la base de données lorsque son numéro de version a été augmenté entre le chargement et la persistance.

stefanglase
la source
Pas via JPAUpdateQuery, ce n'est pas le cas. Donc "Chaque opération qui met à jour l'entité dans la base de données aura ajouté WHERE version = VERSION_THAT_WAS_LOADED_FROM_DATABASE à sa requête" n'est pas vrai.
Pedro Borges
2

Version utilisée pour garantir qu'une seule mise à jour à la fois. Le fournisseur JPA vérifiera la version, si la version attendue augmente déjà, quelqu'un d'autre met déjà à jour l'entité, donc une exception sera levée.

La mise à jour de la valeur de l'entité serait donc plus sûre, plus optimiste.

Si la valeur change fréquemment, vous pouvez envisager de ne pas utiliser le champ de version. Pour un exemple "une entité qui a un champ de compteur, qui augmentera chaque fois qu'une page Web accédera"

uudashr
la source
0

Juste en ajoutant un peu plus d'informations.

JPA gère la version sous le capot pour vous, mais il ne le fait pas lorsque vous mettez à jour votre enregistrement via JPAUpdateClause, dans de tels cas, vous devez ajouter manuellement l'incrément de version à la requête.

Il en va de même pour la mise à jour via JPQL, c'est-à-dire pas une simple modification de l'entité, mais une commande de mise à jour de la base de données même si cela est fait par hibernate

Pedro

Pedro Borges
la source
3
Qu'est-ce que c'est JPAUpdateClause?
Karl Richter
Bonne question, réponse simple: mauvais exemple :) JPAUpdateClause est spécifique à QueryDSL, pensez à exécuter une mise à jour avec JPQL au lieu d'affecter simplement l'état d'une entité dans le code.
Pedro Borges