4

I have an issue with a Spring controller method. It actually does two updates on the same entity which causes the StaleObjectStateException.

The problem is that when I retrieve the Member instance, I think it somehow causes an update (see //UPDATE ONE) of the Advertisement instances (this is not wanted actually) and when I update the Advertisement instance (see //UPDATE TWO), then it throws a StaleObjectStateException.

My question is how can I prevent this exception from occurring in my case (bearing in mind I use Spring Data JPA)?

Here is the Member entity class:

@Entity
public class Member {
...
@OneToMany(fetch = FetchType.LAZY, mappedBy = "member")
private List<Advertisement> advertisements;
...

and in the Advertisement entity class:

@NotNull
@ManyToOne(fetch = FetchType.LAZY)
private Member member;

Here is the controller method:

@RequestMapping(value = "/family/advertisement/edit", method = RequestMethod.POST, produces = "text/html")
    public String editFamilyAdvertisement(@ModelAttribute @Validated(value = Validation.AdvertisementCreation.class) FamilyAdvertisementInfo familyAdvertisementInfo,           BindingResult bindingResult, Model model) {
        Member member = memberService.retrieveCurrentMember();//UPDATE ONE
        if (!advertisementService.advertisementBelongsToMember(familyAdvertisementInfo.getFamilyAdvertisement(), member)) {
            throw new IllegalStateException("advertisement does not belong to member");
        }
        if (bindingResult.hasErrors()) {
            populateModel(model, familyAdvertisementInfo);
            return "family/advertisement/edit";
        }
        familyAdvertisementInfo.getFamilyAdvertisement().setMember(member);
        advertisementService.editFamilyAdvertisement(familyAdvertisementInfo.getFamilyAdvertisement());//UPDATE TWO
        return "redirect:/family/advertisement/edit/" + familyAdvertisementInfo.getFamilyAdvertisement().getId();
    }

Here is the stacktrace:

org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.bignibou.domain.FamilyAdvertisement#1]
    org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:303)
    org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:151)
    org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:76)
    org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:903)
    org.hibernate.internal.SessionImpl.merge(SessionImpl.java:887)
    org.hibernate.internal.SessionImpl.merge(SessionImpl.java:891)
    org.hibernate.ejb.AbstractEntityManagerImpl.merge(AbstractEntityManagerImpl.java:879)
    sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    java.lang.reflect.Method.invoke(Method.java:601)
    org.springframework.orm.jpa.ExtendedEntityManagerCreator$ExtendedEntityManagerInvocationHandler.invoke(ExtendedEntityManagerCreator.java:366)
    com.sun.proxy.$Proxy45.merge(Unknown Source)
    sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    java.lang.reflect.Method.invoke(Method.java:601)
    org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:241)
    com.sun.proxy.$Proxy44.merge(Unknown Source)
    org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:345)
    sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    java.lang.reflect.Method.invoke(Method.java:601)
    org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.executeMethodOn(RepositoryFactorySupport.java:334)
    org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:319)
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:110)
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:155)
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    org.springframework.data.jpa.repository.support.LockModeRepositoryPostProcessor$LockModePopulatingMethodIntercceptor.invoke(LockModeRepositoryPostProcessor.java:91)
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:91)
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204)
    com.sun.proxy.$Proxy52.save(Unknown Source)
    com.bignibou.service.AdvertisementServiceImpl_Roo_Service.ajc$interMethod$com_bignibou_service_AdvertisementServiceImpl_Roo_Service$com_bignibou_service_AdvertisementServiceImpl$updateFamilyAdvertisement(AdvertisementServiceImpl_Roo_Service.aj:58)
    com.bignibou.service.AdvertisementServiceImpl.updateFamilyAdvertisement(AdvertisementServiceImpl.java:1)
    com.bignibou.service.AdvertisementServiceImpl_Roo_Service.ajc$interMethodDispatch1$com_bignibou_service_AdvertisementServiceImpl_Roo_Service$com_bignibou_service_AdvertisementServiceImpl$updateFamilyAdvertisement(AdvertisementServiceImpl_Roo_Service.aj)
    com.bignibou.service.AdvertisementServiceImpl.editFamilyAdvertisement(AdvertisementServiceImpl.java:27)
    com.bignibou.controller.AdvertisementController.editFamilyAdvertisement(AdvertisementController.java:85)
    sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

edit 1:

SQL logs:

2013-04-06 11:23:24,339 [http-bio-8080-exec-9] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - Returning handler method [public java.lang.String com.bignibou.controller.AdvertisementController.editFamilyAdvertisement(com.bignibou.controller.helpers.FamilyAdvertisementInfo,org.springframework.validation.BindingResult,org.springframework.ui.Model)]
Hibernate: 
    /* load com.bignibou.domain.GeolocationPostcode */ select
        geolocatio0_.id as id8_0_,
        geolocatio0_.postcode as postcode8_0_,
        geolocatio0_.version as version8_0_ 
    from
        geolocation_postcode geolocatio0_ 
    where
        geolocatio0_.id=?
Hibernate: 
    /* load com.bignibou.domain.Member */ select
        member0_.id as id6_1_,
        member0_.activated as activated6_1_,
        member0_.address as address6_1_,
        member0_.email as email6_1_,
        member0_.last_connection_date as last4_6_1_,
        member0_.password as password6_1_,
        member0_.registration_date as registra6_6_1_,
        member0_.role as role6_1_,
        member0_.token as token6_1_,
        member0_.version as version6_1_,
        address1_.id as id3_0_,
        address1_.formatted_address as formatted2_3_0_,
        address1_.latitude as latitude3_0_,
        address1_.longitude as longitude3_0_,
        address1_.version as version3_0_ 
    from
        member member0_ 
    left outer join
        address address1_ 
            on member0_.address=address1_.id 
    where
        member0_.id=?
Hibernate: 
    /* load one-to-many com.bignibou.domain.Member.advertisements */ select
        advertisem0_.member as member6_1_,
        advertisem0_.id as id0_1_,
        advertisem0_.id as id0_0_,
        advertisem0_.active as active0_0_,
        advertisem0_.creation_date as creation3_0_0_,
        advertisem0_.description as descript4_0_0_,
        advertisem0_.expiration_date as expiration5_0_0_,
        advertisem0_.member as member0_0_,
        advertisem0_.validated as validated0_0_,
        advertisem0_.version as version0_0_,
        advertisem0_.childminder_status as childmin1_10_0_,
        advertisem0_.clazz_ as clazz_0_ 
    from
        ( select
            id,
            active,
            creation_date,
            description,
            expiration_date,
            validated,
            version,
            member,
            null as childminder_status,
            1 as clazz_ 
        from
            family_advertisement 
        union
        select
            id,
            active,
            creation_date,
            description,
            expiration_date,
            validated,
            version,
            member,
            childminder_status,
            2 as clazz_ 
        from
            childminder_advertisement 
    ) advertisem0_ 
where
    advertisem0_.member=?

Advertisement:

@RooJavaBean
@RooToString
@RooEquals
@RooJpaEntity(inheritanceType = "TABLE_PER_CLASS")
@Entity
@DynamicUpdate
public abstract class Advertisement {

    @ElementCollection
    private Set<ChildcareType> childcareTypes;

    @ManyToMany
    private List<DayToTimeSlot> dayToTimeSlots;

    @NotNull(groups = { Validation.AdvertisementCreation.class })
    @ManyToMany
    private Set<GeolocationPostcode> postcodes;

    @NotNull(groups = { Default.class })
    @Size(min = 6, max = 300, groups = { Default.class, Validation.AdvertisementCreation.class })
    @Column(length = 300)
    private String description;

    @Temporal(TemporalType.TIMESTAMP)
    @DateTimeFormat(pattern = "dd/MM/yyyy HH:mm:ss")
    private Date creationDate;

    @Temporal(TemporalType.TIMESTAMP)
    @DateTimeFormat(pattern = "dd/MM/yyyy HH:mm:ss")
    private Date expirationDate;

    @NotNull(groups = { Default.class })
    private boolean active;

    @NotNull(groups = { Default.class })
    private boolean validated;

    @NotNull
    @ManyToOne(fetch = FetchType.LAZY)
    private Member member;

    public abstract boolean isChildcareTypesValid();

    @AssertTrue(groups = { Validation.AdvertisementCreation.class })
    public boolean isPostcodesValid() {
        return (postcodes != null && !postcodes.isEmpty());
    }

}

FamilyAdvertisement:

@RooJavaBean
@RooToString
@RooEquals
@RooJpaEntity
@Entity
@DynamicUpdate
public class FamilyAdvertisement extends Advertisement {

    @NotNull(groups = Validation.AdvertisementCreation.class)
    @ElementCollection
    private Set<Need> needs;

    @ElementCollection
    private Set<ChildminderStatus> childminderStatuses;

    @AssertTrue
    @Override
    public boolean isChildcareTypesValid() {
        return true;
    }

    @AssertTrue
    public boolean isNeedsValid() {
        if (needs.isEmpty()) {
            return false;
        }
        if (needs.contains(Need.CHILDMINDER_TO_FAMILY)) {
            return false;
        }
        return true;
    }

}

edit 2: I must mention that I use entity versioning as provided by default by Spring Roo + JPA. I am pretty certain this must be taken into account.

edit 3: I have set up a sample application that demonstrates the issue here on github. Anyone wishing to help me can clone the app as follows: git clone https://github.com/balteo/sample-app-gab.

balteo
  • 23,602
  • 63
  • 219
  • 412
  • Have you tried looking at what it's doing with [Hibernate Profiler](http://www.hibernatingrhinos.com/products/hprof) it almost seems like there might be something going on with your session scope. – Kevin Stricker Apr 06 '13 at 01:13
  • Thanks Mootinator. What specific line makes you think there is something wrong with the session scope? – balteo Apr 06 '13 at 07:26
  • Nothing specific, just a smell. Definitely less likely than a mapping problem though, given your case. – Kevin Stricker Apr 07 '13 at 03:26
  • Mootinator: what kind of mapping problem can it be? Can I provide further info about my mapping? If so what? – balteo Apr 08 '13 at 08:19
  • I have edited my post and included a sample app on github that demonstrates the problem. – balteo Apr 09 '13 at 16:33

2 Answers2

9

I had a similar problem. This is the error that I got when running mvn test:

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

It was because, by mistake, I initialised the version field of the entity as NULL:

mysql> select * from lap;
+----+-------------+---------------------+------+------------------+---------+---------+
| id | code        | cumulative_distance | name | partial_distance | version | race_id |
+----+-------------+---------------------+------+------------------+---------+---------+
|  1 | AF0CUL00011 |                1000 | LAP1 |             1000 |    NULL |       1 |
|  2 | AQ000000012 |                2000 | LAP2 |             1000 |    NULL |       2 |
|  3 | AR000000013 |                3000 | LAP3 |             1000 |    NULL |       3 |
|  4 | AR0000N0114 |                4000 | LAP4 |             1000 |    NULL |       4 |
+----+-------------+---------------------+------+------------------+---------+---------+
4 rows in set (0.00 sec)

Once, I initialised the version field with 0, everything worked:

mysql> select * from lap;
+----+-------------+---------------------+------+------------------+---------+---------+
| id | code        | cumulative_distance | name | partial_distance | version | race_id |
+----+-------------+---------------------+------+------------------+---------+---------+
|  1 | AF0CUL00011 |                1000 | LAP1 |             1000 |       0 |       1 |
|  2 | AQ000000012 |                2000 | LAP2 |             1000 |       0 |       2 |
|  3 | AR000000013 |                3000 | LAP3 |             1000 |       0 |       3 |
|  4 | AR0000N0114 |                4000 | LAP4 |             1000 |       0 |       4 |
+----+-------------+---------------------+------+------------------+---------+---------+
4 rows in set (0.00 sec)

I hope it helps, Agustin

Agustin Treceno
  • 351
  • 3
  • 5
3

"org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.bignibou.domain.FamilyAdvertisement#1]" suggests two reasons for the problem - some unexpected update (which would be strange given your are selecting data) or error in JPA mapping.

Please add SQL query logging and post SQL queries which were generated by JPA. Please also post FamilyAdvertisment class. Query logging depends on the JPA provider (see this for hints: How to view the SQL queries issued by JPA?)

Community
  • 1
  • 1
Piotr Kochański
  • 21,862
  • 7
  • 70
  • 77
  • Hi again Piotr. Can you please elaborate on the kind of _error in JPA mapping_ you are referring to? – balteo Apr 08 '13 at 07:23
  • Hi Piotr, I have edited my post and included a sample app on github that demonstrates the problem. – balteo Apr 09 '13 at 16:33