Évitez StaleObjectStateException lorsque l'entité suppression
-
27-10-2019 - |
Question
J'ai 2 threads simultanés qui en même temps entrer dans un service de transaction (printemps).
Utilisation Hibernate, les charges de méthode de service certaines entités, traite ceux-ci, trouve un et supprime de la DB. Le pseudo-code est comme suit:
@Transactional
public MyEntity getAndDelete(String prop) {
List<MyEntity> list = (List<MyEntity>)sessionFactory
.getCurrentSession()
.createCriteria(MyEntity.class)
.add( Restrictions.eq("prop", prop) )
.list();
// process the list, and find one entity
MyEntity entity = findEntity(list);
if (entity != null) {
sessionFactory.getCurrentSession().delete(entity);
}
return entity;
}
Dans le cas où deux threads en même temps passer le même paramètre, tous les deux « trouver » la même entité une fois appellera delete
. L'un d'eux ne parviendront pas jeter un org.hibernate.StaleObjectStateException
lorsque la session est proche.
Je voudrais que les deux fils retourneraient l'entité, sans exception levée. Pour y parvenir, j'ai essayé de verrouillage (avec « select ... mise à jour ») l'entité avant de le supprimer, comme suit:
@Transactional
public MyEntity getAndDelete(String prop) {
List<MyEntity> list = (List<MyEntity>)sessionFactory
.getCurrentSession()
.createCriteria(MyEntity.class)
.add( Restrictions.eq("prop", prop) )
.list();
// process the list, and find one entity
MyEntity entity = findEntity(list);
if (entity != null) {
// reload the entity with "select ...for update"
// to ensure the exception is not thrown
MyEntity locked = (MyEntity)sessionFactory
.getCurrentSession()
.load(MyEntity.class, entity.getId(), new LockOptions(LockMode.PESSIMISTIC_WRITE));
if (locked != null) {
sessionFactory.getCurrentSession().delete(locked);
}
}
return entity;
}
J'utilise load()
au lieu de get()
puisque selon les API de mise en veille prolongée, get retourneraient l'entité si elle est déjà à la session, alors que la charge devrait relire.
Si deux fils entrent en même temps le procédé ci-dessus, l'un d'eux bloque un la de la phase de verrouillage, et lorsque le premier fil ferme la transaction, la seconde est réveillé jeter un org.hibernate.StaleObjectStateException
. Pourquoi?
Pourquoi la charge verrouillée non seulement revenir nulle? Comment pourrais-je y parvenir?
La solution
J'ai passé quelque temps à enquêter sur cette question et je compris enfin ce qui se passe.
Les essais de verrouillage PESSIMISTIC_WRITE de « verrouiller » l'entité qui est déjà chargée dans la session, il n'a pas relue l'objet de la DB. Débogage l'appel, j'ai vu que entity == locked
true
retourné (en Java). Les deux variables ont été pointant vers la même instance.
Pour forcer hiberner à recharger l'entité, il doit être retiré de la première session.
Le code suivant fait le tour:
@Transactional
public MyEntity getAndDelete(String prop) {
List<MyEntity> list = (List<MyEntity>)sessionFactory
.getCurrentSession()
.createCriteria(MyEntity.class)
.add( Restrictions.eq("prop", prop) )
.list();
// process the list, and find one entity
MyEntity entity = findEntity(list);
if (entity != null) {
// Remove the entity from the session.
sessionFactory.getCurrentSession().evict(entity);
// reload the entity with "select ...for update"
MyEntity locked = (MyEntity)sessionFactory
.getCurrentSession()
.get(MyEntity.class, entity.getId(), new LockOptions(LockMode.PESSIMISTIC_WRITE));
if (locked != null) {
sessionFactory.getCurrentSession().delete(locked);
}
}
return entity;
}
Le PESSIMISTIC_WRITE MUT être utilisé avec get
au lieu de load
, car sinon une org.hibernate.ObjectNotFoundException
would levée.