112 votes

La ligne a été mise à jour ou supprimée par une autre transaction (ou la valeur non sauvegardée était incorrecte)

J'ai un projet Java qui s'exécute sur un serveur web. Je rencontre toujours cette exception.

J'ai lu de la documentation et j'ai trouvé que le verrouillage pessimiste (ou optimiste, mais j'ai lu que le pessimiste est meilleur) est la meilleure façon de prévenir cette exception.

Mais je n'ai pas pu trouver d'exemple clair qui explique comment l'utiliser.

Ma méthode est la suivante:

    @Transactional
    public void test(Email email, String subject) {
        getEmailById(String id);
        email.setSubject(subject);
        updateEmail(email);
    }

alors que:

  • Email est une classe Hibernate (ce sera une table dans la base de données)
  • getEmailById(String id) est une fonction qui renvoie un email (cette méthode n'est pas annotée avec @Transactional)
  • updateEmail(email): est une méthode qui met à jour l'email.

Remarque : J'utilise Hibernate pour sauvegarder, mettre à jour & etc (exemple : session.getcurrentSession.save(email))

L'exception :

ERREUR 2011-12-21 15:29:24,910 Impossible de synchroniser l'état de la base de données avec la session [myScheduler-1]
org.hibernate.StaleObjectStateException: La ligne a été mise à jour ou supprimée par une autre transaction (ou l'association d'une valeur non enregistrée était incorrecte) : [email#21]
    at org.hibernate.persister.entity.AbstractEntityPersister.check(AbstractEntityPersister.java:1792)
    at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2435)
    at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:2335)
    at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2635)
    at org.hibernate.action.EntityUpdateAction.execute(EntityUpdateAction.java:115)
    at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:279)
    at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:263)
    at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:168)
    at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:321)
    at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:50)
    at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1027)
    at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:365)
    at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:137)
    at org.springframework.orm.hibernate3.HibernateTransactionManager.doCommit(HibernateTransactionManager.java:656)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:754)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:723)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:393)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:120)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202)
    at $Proxy130.generateEmail(Unknown Source)
    at com.admtel.appserver.tasks.EmailSender.run(EmailNotificationSender.java:33)
    at com.admtel.appserver.tasks.EmailSender$$FastClassByCGLIB$$ea0d4fc2.invoke()
    at net.sf.cglib.proxy.MethodProxy.invoke(MethodProxy.java:149)
    at org.springframework.aop.framework.Cglib2AopProxy$CglibMethodInvocation.invokeJoinpoint(Cglib2AopProxy.java:688)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150)
    at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:55)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:161)
    at org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor.invoke(AfterReturningAdviceInterceptor.java:50)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:161)
    at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:50)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:161)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:89)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.Cglib2AopProxy$DynamicAdvisedInterceptor.intercept(Cglib2AopProxy.java:621)
    at com.admtel.appserver.tasks.EmailNotificationSender$$EnhancerByCGLIB$$33eb7303.run()
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Méthode native)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.springframework.util.MethodInvoker.invoke(MethodInvoker.java:273)
    at org.springframework.scheduling.support.MethodInvokingRunnable.run(MethodInvokingRunnable.java:65)
    at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:51)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:441)
    at java.util.concurrent.FutureTask$Sync.innerRunAndReset(FutureTask.java:317)
    at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:150)
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$101(ScheduledThreadPoolExecutor.java:98)
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.runPeriodic(ScheduledThreadPoolExecutor.java:180)
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:204)
    at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
    at java.lang.Thread.run(Thread.java:680)
ERREUR 2011-12-21 15:29:24,915 [exception levée < EmailNotificationSender.run() > message d'exception Objet de classe [Email] avec identifiant [211] : l'échec du verrouillage optimiste; l'exception imbriquée est org.hibernate.StaleObjectStateException: La ligne a été mise à jour ou supprimée par une autre transaction (ou l'association d'une valeur non enregistrée était incorrecte) : [Email#21] avec paramètres ] [myScheduler-1]
org.springframework.orm.hibernate3.HibernateOptimisticLockingFailureException: Objet de classe [Email] avec identifiant [21] : l'échec du verrouillage optimiste; exception imbriquée est

74voto

Santosh Points 9794

Le verrouillage pessimiste n'est généralement pas recommandé et est très coûteux en termes de performances du côté de la base de données. Le problème que vous avez mentionné (la partie du code) quelques éléments ne sont pas clairs tels que :

  • Si votre code est accédé par plusieurs threads en même temps.
  • Comment créez-vous l'objet session (je ne suis pas sûr si vous utilisez Spring)?

Les objets Session de Hibernate ne sont PAS thread-safe. Donc, s'il y a plusieurs threads accédant à la même session et essayant de mettre à jour la même entité de base de données, votre code peut potentiellement se retrouver dans une situation d'erreur comme celle-ci.

Donc ce qui se passe ici c'est que plus d'un thread essaie de mettre à jour la même entité, un thread réussit et lorsque le prochain thread va commettre les données, il voit qu'elles ont déjà été modifiées et finit par lancer StaleObjectStateException.

EDITION:

Il y a un moyen d'utiliser le Verrouillage Pessimiste dans Hibernate. Consultez ce lien. Mais il semble y avoir un problème avec ce mécanisme. J'ai rencontré un bug dans hibernate (HHH-5275), cependant. Le scénario mentionné dans le bug est le suivant :

Deux threads lisent le même enregistrement de base de données ; l'un de ces threads devrait utiliser un verrouillage pessimiste bloquant ainsi l'autre thread. Mais les deux threads peuvent lire l'enregistrement de la base de données causant l'échec du test.

Cela se rapproche beaucoup de ce à quoi vous êtes confronté. Veuillez essayer cela, si cela ne fonctionne pas, la seule façon à laquelle je puisse penser est d'utiliser des requêtes SQL natives où vous pouvez réaliser un verrouillage pessimiste dans la base de données postgres avec la requête SELECT FOR UPDATE.

22voto

Lyju I Edwinson Points 101

Nous avons un gestionnaire de file d'attente qui interroge les données et les transmet aux gestionnaires pour le traitement. Pour éviter de reprendre les mêmes événements, le gestionnaire de file d'attente verrouille l'enregistrement dans la base de données avec un état LOCKED.

    void poll() {
        record = dao.getLockedEntity();
        queue(record);
    }

Cette méthode n'était pas transactionnelle mais dao.getLockedEntity() était transactionnelle avec REQUIRED.

Tout allait bien et en route, après quelques mois en production, une exception de verrouillage optimiste est survenue.

Après beaucoup de débogage et de vérifications détaillées, nous avons pu découvrir que quelqu'un avait modifié le code comme ceci :

    @Transactional(propagation=Propagation.REQUIRED, readOnly=false)
    void poll() {
        record = dao.getLockedEntity();
        queue(record);              
    }

Ainsi, l'enregistrement était mis en file d'attente même avant que la transaction dans dao.getLockedEntity() ne soit validée (il utilise la même transaction de la méthode poll) et l'objet a été modifié en dessous par les gestionnaires (différents threads) d'ici le moment où la transaction de la méthode poll() est validée.

Nous avons corrigé le problème et tout semble bien fonctionner maintenant. J'ai pensé le partager car les exceptions de verrouillage optimiste peuvent être déroutantes et difficiles à déboguer.

18voto

tvanfosson Points 268301

Il semble que vous n'utilisiez pas réellement l'e-mail que vous récupérez de la base de données, mais une version plus ancienne que vous obtenez en tant que paramètre. Ce qui est utilisé pour le contrôle de version sur la ligne a changé entre la dernière version récupérée et la mise à jour que vous effectuez.

Vous voulez probablement que votre code ressemble plus à ceci:

    @Transactional
    public void test(String id, String subject) {
       Email email = getEmailById(id);
       email.setSubject(subject);
       updateEmail(email);
    }

13voto

MF Wolf Points 91

J'ai eu ce problème sur mon projet.

Après avoir mis en place le verrouillage optimiste, j'ai obtenu la même exception. Mon erreur était de ne pas avoir supprimé le setter du champ devenu @Version. Comme le setter était appelé dans l'espace java, la valeur du champ ne correspondait plus à celle générée par la base de données. En gros, les champs de version ne correspondaient plus. À ce stade, toute modification apportée à l'entité aboutissait à :

org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)

Je utilise H2 en base de données en mémoire et Hibernate.

6voto

JB Nizet Points 250258

Cette exception est probablement causée par un verrouillage optimiste (ou par un bug dans votre code). Vous l'utilisez probablement sans le savoir. Et votre pseudo-code (qui devrait être remplacé par du code réel pour pouvoir diagnostiquer le problème) est incorrect. Hibernate enregistre automatiquement toutes les modifications apportées aux entités attachées. Vous ne devriez jamais appeler update, merge ou saveOrUpdate sur une entité attachée. Faites simplement

Email email = session.get(emailId);
email.setSubject(subject);

Pas besoin d'appeler update. Hibernate va automatiquement appliquer les changements avant de valider la transaction.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X