Reputation: 24481
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?
Upvotes: 1
Views: 1947
Reputation: 24481
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.
Upvotes: 1
Reputation: 19543
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
Upvotes: 1
Reputation: 41143
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)
Upvotes: 0