Question

I'm using Spring-Data JPA and Spring-MVC with a RESTful interface. I'm trying to implement a basic CRUD controller. I'm having some difficulty figuring out the best way to implement the "Update".

My basic controller method is straight forward:

@RequestMapping( method=RequestMethod.POST, value="updateUser", produces=MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public User update( @RequestBody final User user){
    userRepository.save(user);
    return user;
}

However, this only seems to provide a "create" - not an "update". Everytime the method is called, a new user is created in the DB, even if I specify the User PK in the JSON object.

From a quick look at the SimpleJpaRepository class, it creates a new object whenever the "version" field is missing. However, if I force the "version" field to have a value, I get an exception (not surprisingly):

Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.ia.domain.User#5]
    at org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:303)
    at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:151)
    at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:76)
    at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:914)
    at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:898)
    at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:902)
    at org.hibernate.ejb.AbstractEntityManagerImpl.merge(AbstractEntityManagerImpl.java:889)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)

I realize that one option is to first query the DB for the existing User object (based on the submitted PK), then copy all the fields over and then save that object, but that does not seem like the right way. I presume there must be a way for me to "merge" my User object and just update it, but am not entirely sure how.

Is there a simple way of doing this?

Was it helpful?

Solution 2

After more research and debugging and trial & error, it turns out the solution is very simple. The value of the version field of the entity being persisted must match what is currently in the DB. If the values don't match Hibernate presumes that something else updated the DB and consequently throws the org.hibernate.StaleObjectStateException.

Ensuring that the JSON being submitted has a matching PK and version value as the row in the DB, then the repo.save(user) will update the existing row. If the version field is null, the entity is persisted as a new row.

OTHER TIPS

Tracking the error to the hibernate code I found there is problem related to the version

else if ( isVersionChanged( entity, source, persister, target ) ) {
299             if ( source.getFactory().getStatistics().isStatisticsEnabled() ) {
300                 source.getFactory().getStatisticsImplementor()
301                         .optimisticFailure( entityName );
302             }
303             throw new StaleObjectStateException( entityName, id );
304         }

/VersionChanges is

342     boolean changed = ! persister.getVersionType().isSame(
343             persister.getVersion( target ),
344             persister.getVersion( entity )
345     );

The error is coming from different version of the entity, Consider using to save the standard method of JPARepository.

@Transactional
public <S extends T> S save(S entity)

Also update your question with the code for userRepository, as I think saveUser is your own implementation

I don't think you should "force the version field to have value". They're there for the purpose of optimistic locking. Other transaction could be modifying your record and the version you're working at become stale.

As with your entity object not updating, Spring might failed to detect it's a detached entity because the primary key field mapping was wrong / not setup. It could also be due to the entity never been persisted in the first place (hence it's in new state)

I believe you want to use userRepository.merge(user).

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top